Skip to main content

amaters_net/
tls_crypto.rs

1//! Pure Rust cryptographic primitives for encrypted PEM key handling
2//!
3//! This module provides:
4//! - Encrypted PEM format detection (PKCS#8, legacy OpenSSL)
5//! - PKCS#8 encrypted key parsing and decryption (ASN.1/DER)
6//! - Legacy OpenSSL encrypted key decryption
7//! - PBKDF2-HMAC-SHA256 and PBKDF2-HMAC-SHA1 key derivation
8//! - AES-CBC decryption (128/192/256-bit)
9//! - Pure Rust SHA-256, SHA-1, MD5 hash implementations
10//! - EVP_BytesToKey key derivation (legacy OpenSSL compatibility)
11//!
12//! All implementations are 100% pure Rust with zero C/Fortran dependencies.
13
14use rustls::pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer};
15use tracing::debug;
16
17use crate::error::{NetError, NetResult};
18
19// ============================================================================
20// Encrypted PEM support (Pure Rust)
21// ============================================================================
22
23/// Detected encryption format for a PEM file
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum EncryptedPemFormat {
26    /// Not encrypted — standard PEM
27    NotEncrypted,
28    /// PKCS#8 encrypted private key (`BEGIN ENCRYPTED PRIVATE KEY`)
29    Pkcs8Encrypted,
30    /// Legacy OpenSSL encrypted format (`Proc-Type: 4,ENCRYPTED`)
31    LegacyEncrypted,
32}
33
34/// Detect whether a PEM string contains an encrypted private key
35pub fn detect_encrypted_pem(pem_str: &str) -> EncryptedPemFormat {
36    if pem_str.contains("-----BEGIN ENCRYPTED PRIVATE KEY-----") {
37        EncryptedPemFormat::Pkcs8Encrypted
38    } else if pem_str.contains("Proc-Type: 4,ENCRYPTED") {
39        EncryptedPemFormat::LegacyEncrypted
40    } else {
41        EncryptedPemFormat::NotEncrypted
42    }
43}
44
45/// Resolve effective password: use provided password or fall back to env var
46pub(crate) fn resolve_password(password: &str) -> NetResult<String> {
47    if !password.is_empty() {
48        return Ok(password.to_string());
49    }
50
51    match std::env::var("AMATERS_KEY_PASSWORD") {
52        Ok(env_pw) if !env_pw.is_empty() => {
53            debug!("Using password from AMATERS_KEY_PASSWORD environment variable");
54            Ok(env_pw)
55        }
56        _ => Err(NetError::InvalidCertificate(
57            "Key is encrypted but no password provided. Set AMATERS_KEY_PASSWORD or pass a password."
58                .to_string(),
59        )),
60    }
61}
62
63/// Parse DEK-Info header from legacy OpenSSL encrypted PEM
64///
65/// Returns (algorithm_name, iv_bytes)
66pub fn parse_dek_info(pem_str: &str) -> NetResult<(String, Vec<u8>)> {
67    for line in pem_str.lines() {
68        let trimmed = line.trim();
69        if let Some(rest) = trimmed.strip_prefix("DEK-Info:") {
70            let rest = rest.trim();
71            let parts: Vec<&str> = rest.splitn(2, ',').collect();
72            if parts.len() != 2 {
73                return Err(NetError::InvalidCertificate(
74                    "Malformed DEK-Info header: expected 'algorithm,IV'".to_string(),
75                ));
76            }
77            let algorithm = parts[0].trim().to_string();
78            let iv_hex = parts[1].trim();
79            let iv = hex_decode(iv_hex).map_err(|e| {
80                NetError::InvalidCertificate(format!("Invalid IV hex in DEK-Info: {e}"))
81            })?;
82            return Ok((algorithm, iv));
83        }
84    }
85    Err(NetError::InvalidCertificate(
86        "No DEK-Info header found in legacy encrypted PEM".to_string(),
87    ))
88}
89
90/// Decrypt a PKCS#8 encrypted PEM key
91///
92/// Parses the ASN.1 DER structure to extract:
93/// - Encryption algorithm (PBES2 with PBKDF2 + AES-CBC)
94/// - PBKDF2 parameters (salt, iteration count)
95/// - AES IV
96///
97/// Then derives the key and decrypts.
98pub(crate) fn decrypt_pkcs8_encrypted_pem(
99    pem_str: &str,
100    password: &str,
101) -> NetResult<PrivateKeyDer<'static>> {
102    let der_data = extract_pem_body(pem_str, "ENCRYPTED PRIVATE KEY")?;
103    let decrypted = decrypt_pkcs8_der(&der_data, password.as_bytes())?;
104    Ok(PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(decrypted)))
105}
106
107/// Decrypt a legacy OpenSSL encrypted PEM key
108pub(crate) fn decrypt_legacy_encrypted_pem(
109    pem_str: &str,
110    password: &str,
111) -> NetResult<PrivateKeyDer<'static>> {
112    let (algorithm, iv) = parse_dek_info(pem_str)?;
113
114    // Determine key size from algorithm
115    let key_len = match algorithm.as_str() {
116        "AES-256-CBC" => 32,
117        "AES-128-CBC" => 16,
118        "AES-192-CBC" => 24,
119        "DES-EDE3-CBC" => 24,
120        other => {
121            return Err(NetError::InvalidCertificate(format!(
122                "Unsupported encryption algorithm: {other}"
123            )));
124        }
125    };
126
127    // Extract the base64 body (skip headers and Proc-Type/DEK-Info lines)
128    let body = extract_legacy_pem_body(pem_str)?;
129
130    // Legacy OpenSSL uses EVP_BytesToKey for key derivation (MD5-based)
131    let derived_key = evp_bytes_to_key_md5(password.as_bytes(), &iv[..8], key_len);
132
133    // Decrypt
134    let decrypted = if algorithm.starts_with("AES") {
135        aes_cbc_decrypt(&body, &derived_key, &iv)?
136    } else if algorithm == "DES-EDE3-CBC" {
137        return Err(NetError::InvalidCertificate(
138            "DES-EDE3-CBC is not supported in pure Rust mode. Please re-encrypt with AES-256-CBC."
139                .to_string(),
140        ));
141    } else {
142        return Err(NetError::InvalidCertificate(format!(
143            "Unsupported encryption algorithm: {algorithm}"
144        )));
145    };
146
147    // Remove PKCS#7 padding
148    let unpadded = remove_pkcs7_padding(&decrypted)?;
149
150    // The decrypted content is a PKCS#1 RSA private key in DER format
151    // (legacy format encrypts the inner key, not wrapped in PKCS#8)
152    // Try to interpret as PKCS#8 first, fall back to PKCS#1
153    if is_pkcs8_key_der(unpadded) {
154        Ok(PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(
155            unpadded.to_vec(),
156        )))
157    } else {
158        // Legacy encrypted RSA keys contain raw PKCS#1
159        Ok(PrivateKeyDer::Pkcs1(unpadded.to_vec().into()))
160    }
161}
162
163/// Extract base64-encoded body from a PEM block with given label
164fn extract_pem_body(pem_str: &str, label: &str) -> NetResult<Vec<u8>> {
165    let begin_marker = format!("-----BEGIN {label}-----");
166    let end_marker = format!("-----END {label}-----");
167
168    let start = pem_str.find(&begin_marker).ok_or_else(|| {
169        NetError::InvalidCertificate(format!("Missing PEM header: {begin_marker}"))
170    })? + begin_marker.len();
171
172    let end = pem_str
173        .find(&end_marker)
174        .ok_or_else(|| NetError::InvalidCertificate(format!("Missing PEM footer: {end_marker}")))?;
175
176    let body = &pem_str[start..end];
177    let b64: String = body.chars().filter(|c| !c.is_whitespace()).collect();
178    base64_decode_pure(&b64)
179        .map_err(|e| NetError::InvalidCertificate(format!("Invalid base64 in PEM: {e}")))
180}
181
182/// Extract base64 body from legacy encrypted PEM (skip Proc-Type and DEK-Info headers)
183fn extract_legacy_pem_body(pem_str: &str) -> NetResult<Vec<u8>> {
184    let mut in_body = false;
185    let mut past_headers = false;
186    let mut b64 = String::new();
187
188    for line in pem_str.lines() {
189        let trimmed = line.trim();
190        if trimmed.starts_with("-----BEGIN ") && trimmed.ends_with("-----") {
191            in_body = true;
192            continue;
193        }
194        if trimmed.starts_with("-----END ") && trimmed.ends_with("-----") {
195            break;
196        }
197        if !in_body {
198            continue;
199        }
200        // Skip Proc-Type and DEK-Info headers
201        if trimmed.starts_with("Proc-Type:") || trimmed.starts_with("DEK-Info:") {
202            continue;
203        }
204        // Skip empty line after headers
205        if !past_headers && trimmed.is_empty() {
206            past_headers = true;
207            continue;
208        }
209        past_headers = true;
210        b64.push_str(trimmed);
211    }
212
213    base64_decode_pure(&b64)
214        .map_err(|e| NetError::InvalidCertificate(format!("Invalid base64 in legacy PEM: {e}")))
215}
216
217/// Check if DER data looks like PKCS#8 (starts with SEQUENCE containing version INTEGER)
218fn is_pkcs8_key_der(data: &[u8]) -> bool {
219    // PKCS#8 PrivateKeyInfo starts with SEQUENCE > INTEGER (version 0)
220    // then AlgorithmIdentifier SEQUENCE
221    if data.len() < 4 {
222        return false;
223    }
224    // 0x30 = SEQUENCE tag
225    if data[0] != 0x30 {
226        return false;
227    }
228    // Parse length to find content start
229    let (_, content_offset) = match parse_asn1_length(&data[1..]) {
230        Ok(v) => v,
231        Err(_) => return false,
232    };
233    let content_start = 1 + content_offset;
234    if content_start >= data.len() {
235        return false;
236    }
237    // First element should be INTEGER (0x02) with value 0 (version)
238    if data[content_start] == 0x02 {
239        // Check for AlgorithmIdentifier after the version
240        let ver_start = content_start + 1;
241        if ver_start < data.len() {
242            let (ver_len, ver_len_size) = match parse_asn1_length(&data[ver_start..]) {
243                Ok(v) => v,
244                Err(_) => return false,
245            };
246            let after_ver = ver_start + ver_len_size + ver_len;
247            if after_ver < data.len() && data[after_ver] == 0x30 {
248                return true;
249            }
250        }
251    }
252    false
253}
254
255// ============================================================================
256// PKCS#8 encrypted DER parsing (ASN.1)
257// ============================================================================
258
259/// Parse ASN.1 length encoding, returns (length, bytes_consumed)
260pub(crate) fn parse_asn1_length(data: &[u8]) -> NetResult<(usize, usize)> {
261    if data.is_empty() {
262        return Err(NetError::InvalidCertificate(
263            "ASN.1 parse error: unexpected end of data for length".to_string(),
264        ));
265    }
266    if data[0] < 0x80 {
267        Ok((data[0] as usize, 1))
268    } else if data[0] == 0x80 {
269        Err(NetError::InvalidCertificate(
270            "ASN.1 indefinite length not supported".to_string(),
271        ))
272    } else {
273        let num_bytes = (data[0] & 0x7f) as usize;
274        if num_bytes > 4 || num_bytes + 1 > data.len() {
275            return Err(NetError::InvalidCertificate(
276                "ASN.1 parse error: length too large or truncated".to_string(),
277            ));
278        }
279        let mut length = 0usize;
280        for &b in &data[1..=num_bytes] {
281            length = length
282                .checked_shl(8)
283                .ok_or_else(|| NetError::InvalidCertificate("ASN.1 length overflow".to_string()))?
284                .checked_add(b as usize)
285                .ok_or_else(|| NetError::InvalidCertificate("ASN.1 length overflow".to_string()))?;
286        }
287        Ok((length, num_bytes + 1))
288    }
289}
290
291/// Skip over an ASN.1 TLV (tag-length-value), returning bytes consumed
292fn skip_asn1_tlv(data: &[u8]) -> NetResult<usize> {
293    if data.is_empty() {
294        return Err(NetError::InvalidCertificate(
295            "ASN.1 parse error: empty TLV".to_string(),
296        ));
297    }
298    let (len, len_size) = parse_asn1_length(&data[1..])?;
299    Ok(1 + len_size + len)
300}
301
302/// PKCS#8 EncryptedPrivateKeyInfo structure:
303/// SEQUENCE {
304///   SEQUENCE { -- encryptionAlgorithm (AlgorithmIdentifier)
305///     OID -- PBES2 (1.2.840.113549.1.5.13)
306///     SEQUENCE { -- PBES2-params
307///       SEQUENCE { -- keyDerivationFunc (AlgorithmIdentifier)
308///         OID -- PBKDF2 (1.2.840.113549.1.5.12)
309///         SEQUENCE { -- PBKDF2-params
310///           OCTET STRING -- salt
311///           INTEGER -- iterationCount
312///           [optional INTEGER -- keyLength]
313///           [optional SEQUENCE -- prf AlgorithmIdentifier, default HMAC-SHA1]
314///         }
315///       }
316///       SEQUENCE { -- encryptionScheme (AlgorithmIdentifier)
317///         OID -- e.g. AES-256-CBC (2.16.840.1.101.3.4.1.42)
318///         OCTET STRING -- IV
319///       }
320///     }
321///   }
322///   OCTET STRING -- encryptedData
323/// }
324fn decrypt_pkcs8_der(data: &[u8], password: &[u8]) -> NetResult<Vec<u8>> {
325    let mut pos = 0;
326
327    // Outer SEQUENCE
328    if data.get(pos) != Some(&0x30) {
329        return Err(NetError::InvalidCertificate(
330            "PKCS#8 encrypted: expected outer SEQUENCE".to_string(),
331        ));
332    }
333    pos += 1;
334    let (_outer_len, outer_len_size) = parse_asn1_length(&data[pos..])?;
335    pos += outer_len_size;
336
337    // encryptionAlgorithm SEQUENCE
338    if data.get(pos) != Some(&0x30) {
339        return Err(NetError::InvalidCertificate(
340            "PKCS#8 encrypted: expected algorithm SEQUENCE".to_string(),
341        ));
342    }
343    pos += 1;
344    let (algo_seq_len, algo_len_size) = parse_asn1_length(&data[pos..])?;
345    pos += algo_len_size;
346    let algo_seq_end = pos + algo_seq_len;
347
348    // Read PBES2 OID
349    let pbes2_oid = parse_asn1_oid(&data[pos..])?;
350    pos += skip_asn1_tlv(&data[pos..])?;
351
352    // Verify PBES2 OID: 1.2.840.113549.1.5.13
353    if pbes2_oid != [42, 134, 72, 134, 247, 13, 1, 5, 13] {
354        return Err(NetError::InvalidCertificate(format!(
355            "Unsupported encryption algorithm OID (expected PBES2): {:?}",
356            pbes2_oid
357        )));
358    }
359
360    // PBES2-params SEQUENCE
361    if data.get(pos) != Some(&0x30) {
362        return Err(NetError::InvalidCertificate(
363            "PKCS#8 encrypted: expected PBES2-params SEQUENCE".to_string(),
364        ));
365    }
366    pos += 1;
367    let (_pbes2_len, pbes2_len_size) = parse_asn1_length(&data[pos..])?;
368    pos += pbes2_len_size;
369
370    // keyDerivationFunc SEQUENCE
371    if data.get(pos) != Some(&0x30) {
372        return Err(NetError::InvalidCertificate(
373            "PKCS#8 encrypted: expected KDF SEQUENCE".to_string(),
374        ));
375    }
376    pos += 1;
377    let (kdf_seq_len, kdf_len_size) = parse_asn1_length(&data[pos..])?;
378    pos += kdf_len_size;
379    let kdf_seq_end = pos + kdf_seq_len;
380
381    // Read PBKDF2 OID
382    let pbkdf2_oid = parse_asn1_oid(&data[pos..])?;
383    pos += skip_asn1_tlv(&data[pos..])?;
384
385    // Verify PBKDF2 OID: 1.2.840.113549.1.5.12
386    if pbkdf2_oid != [42, 134, 72, 134, 247, 13, 1, 5, 12] {
387        return Err(NetError::InvalidCertificate(format!(
388            "Unsupported KDF OID (expected PBKDF2): {:?}",
389            pbkdf2_oid
390        )));
391    }
392
393    // PBKDF2-params SEQUENCE
394    if data.get(pos) != Some(&0x30) {
395        return Err(NetError::InvalidCertificate(
396            "PKCS#8 encrypted: expected PBKDF2-params SEQUENCE".to_string(),
397        ));
398    }
399    pos += 1;
400    let (pbkdf2_params_len, pbkdf2_params_len_size) = parse_asn1_length(&data[pos..])?;
401    pos += pbkdf2_params_len_size;
402    let pbkdf2_params_end = pos + pbkdf2_params_len;
403
404    // salt OCTET STRING
405    let salt = parse_asn1_octet_string(&data[pos..])?;
406    pos += skip_asn1_tlv(&data[pos..])?;
407
408    // iterationCount INTEGER
409    let iterations = parse_asn1_integer_value(&data[pos..])?;
410    pos += skip_asn1_tlv(&data[pos..])?;
411
412    // Optional: keyLength INTEGER and PRF AlgorithmIdentifier
413    // We detect the PRF to decide SHA-1 vs SHA-256
414    let mut prf_is_sha256 = false;
415    while pos < pbkdf2_params_end {
416        let tag = data.get(pos).copied().unwrap_or(0);
417        if tag == 0x02 {
418            // keyLength — skip
419            pos += skip_asn1_tlv(&data[pos..])?;
420        } else if tag == 0x30 {
421            // PRF AlgorithmIdentifier
422            let prf_inner_start = pos + 1;
423            let (prf_seq_len, prf_seq_len_size) = parse_asn1_length(&data[prf_inner_start..])?;
424            let prf_content_start = prf_inner_start + prf_seq_len_size;
425            let prf_oid = parse_asn1_oid(&data[prf_content_start..])?;
426            // HMAC-SHA-256: 1.2.840.113549.2.9
427            if prf_oid == [42, 134, 72, 134, 247, 13, 2, 9] {
428                prf_is_sha256 = true;
429            }
430            // HMAC-SHA-1: 1.2.840.113549.2.7  (default if not SHA-256)
431            pos += skip_asn1_tlv(&data[pos..])?;
432        } else {
433            pos += skip_asn1_tlv(&data[pos..])?;
434        }
435    }
436    pos = kdf_seq_end;
437
438    // encryptionScheme SEQUENCE
439    if data.get(pos) != Some(&0x30) {
440        return Err(NetError::InvalidCertificate(
441            "PKCS#8 encrypted: expected encryption scheme SEQUENCE".to_string(),
442        ));
443    }
444    pos += 1;
445    let (enc_seq_len, enc_len_size) = parse_asn1_length(&data[pos..])?;
446    pos += enc_len_size;
447
448    let enc_oid = parse_asn1_oid(&data[pos..])?;
449    pos += skip_asn1_tlv(&data[pos..])?;
450
451    // Determine AES key size from encryption OID
452    // AES-128-CBC: 2.16.840.1.101.3.4.1.2
453    // AES-192-CBC: 2.16.840.1.101.3.4.1.22
454    // AES-256-CBC: 2.16.840.1.101.3.4.1.42
455    let key_len = match enc_oid.as_slice() {
456        [96, 134, 72, 1, 101, 3, 4, 1, 2] => 16,
457        [96, 134, 72, 1, 101, 3, 4, 1, 22] => 24,
458        [96, 134, 72, 1, 101, 3, 4, 1, 42] => 32,
459        _ => {
460            return Err(NetError::InvalidCertificate(format!(
461                "Unsupported encryption scheme OID: {:?}",
462                enc_oid
463            )));
464        }
465    };
466
467    // IV OCTET STRING
468    let iv = parse_asn1_octet_string(&data[pos..])?;
469
470    // Move past the algorithm sequence to get encrypted data
471    let pos = algo_seq_end;
472
473    // encryptedData OCTET STRING
474    let encrypted_data = parse_asn1_octet_string(&data[pos..])?;
475
476    // Derive key using PBKDF2
477    let derived_key = if prf_is_sha256 {
478        pbkdf2_hmac_sha256(password, &salt, iterations as u32, key_len)
479    } else {
480        pbkdf2_hmac_sha1(password, &salt, iterations as u32, key_len)
481    };
482
483    // Decrypt with AES-CBC
484    let decrypted = aes_cbc_decrypt(&encrypted_data, &derived_key, &iv)?;
485
486    // Remove PKCS#7 padding
487    let unpadded = remove_pkcs7_padding(&decrypted)?;
488
489    Ok(unpadded.to_vec())
490}
491
492/// Parse an ASN.1 OID, returning raw OID bytes (not decoded dotted form)
493fn parse_asn1_oid(data: &[u8]) -> NetResult<Vec<u8>> {
494    if data.is_empty() || data[0] != 0x06 {
495        return Err(NetError::InvalidCertificate(
496            "ASN.1 parse error: expected OID tag (0x06)".to_string(),
497        ));
498    }
499    let (len, len_size) = parse_asn1_length(&data[1..])?;
500    let start = 1 + len_size;
501    let end = start + len;
502    if end > data.len() {
503        return Err(NetError::InvalidCertificate(
504            "ASN.1 parse error: OID data truncated".to_string(),
505        ));
506    }
507    Ok(data[start..end].to_vec())
508}
509
510/// Parse an ASN.1 OCTET STRING, returning the contained bytes
511fn parse_asn1_octet_string(data: &[u8]) -> NetResult<Vec<u8>> {
512    if data.is_empty() || data[0] != 0x04 {
513        return Err(NetError::InvalidCertificate(
514            "ASN.1 parse error: expected OCTET STRING tag (0x04)".to_string(),
515        ));
516    }
517    let (len, len_size) = parse_asn1_length(&data[1..])?;
518    let start = 1 + len_size;
519    let end = start + len;
520    if end > data.len() {
521        return Err(NetError::InvalidCertificate(
522            "ASN.1 parse error: OCTET STRING data truncated".to_string(),
523        ));
524    }
525    Ok(data[start..end].to_vec())
526}
527
528/// Parse an ASN.1 INTEGER value as usize
529fn parse_asn1_integer_value(data: &[u8]) -> NetResult<usize> {
530    if data.is_empty() || data[0] != 0x02 {
531        return Err(NetError::InvalidCertificate(
532            "ASN.1 parse error: expected INTEGER tag (0x02)".to_string(),
533        ));
534    }
535    let (len, len_size) = parse_asn1_length(&data[1..])?;
536    let start = 1 + len_size;
537    let end = start + len;
538    if end > data.len() {
539        return Err(NetError::InvalidCertificate(
540            "ASN.1 parse error: INTEGER data truncated".to_string(),
541        ));
542    }
543    let mut value = 0usize;
544    for &b in &data[start..end] {
545        value = value
546            .checked_shl(8)
547            .ok_or_else(|| NetError::InvalidCertificate("ASN.1 INTEGER overflow".to_string()))?
548            .checked_add(b as usize)
549            .ok_or_else(|| NetError::InvalidCertificate("ASN.1 INTEGER overflow".to_string()))?;
550    }
551    Ok(value)
552}
553
554// ============================================================================
555// Pure Rust PBKDF2 implementations
556// ============================================================================
557
558/// PBKDF2-HMAC-SHA256 key derivation (pure Rust)
559pub fn pbkdf2_hmac_sha256(
560    password: &[u8],
561    salt: &[u8],
562    iterations: u32,
563    key_len: usize,
564) -> Vec<u8> {
565    let mut result = Vec::with_capacity(key_len);
566    let mut block_num = 1u32;
567
568    while result.len() < key_len {
569        let block = pbkdf2_f_sha256(password, salt, iterations, block_num);
570        let needed = key_len - result.len();
571        result.extend_from_slice(&block[..needed.min(32)]);
572        block_num += 1;
573    }
574
575    result.truncate(key_len);
576    result
577}
578
579/// PBKDF2 F function for SHA-256
580fn pbkdf2_f_sha256(password: &[u8], salt: &[u8], iterations: u32, block_num: u32) -> [u8; 32] {
581    // U_1 = HMAC(password, salt || INT(block_num))
582    let mut msg = Vec::with_capacity(salt.len() + 4);
583    msg.extend_from_slice(salt);
584    msg.extend_from_slice(&block_num.to_be_bytes());
585
586    let mut u_prev = hmac_sha256(password, &msg);
587    let mut result = u_prev;
588
589    for _ in 1..iterations {
590        let u_curr = hmac_sha256(password, &u_prev);
591        for (r, c) in result.iter_mut().zip(u_curr.iter()) {
592            *r ^= c;
593        }
594        u_prev = u_curr;
595    }
596
597    result
598}
599
600/// HMAC-SHA256 (pure Rust)
601fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
602    const BLOCK_SIZE: usize = 64;
603
604    let normalized_key = if key.len() > BLOCK_SIZE {
605        let h = sha256(key);
606        let mut k = [0u8; BLOCK_SIZE];
607        k[..32].copy_from_slice(&h);
608        k
609    } else {
610        let mut k = [0u8; BLOCK_SIZE];
611        k[..key.len()].copy_from_slice(key);
612        k
613    };
614
615    let mut ipad = [0x36u8; BLOCK_SIZE];
616    let mut opad = [0x5cu8; BLOCK_SIZE];
617    for i in 0..BLOCK_SIZE {
618        ipad[i] ^= normalized_key[i];
619        opad[i] ^= normalized_key[i];
620    }
621
622    // inner = SHA256(ipad || message)
623    let mut inner_msg = Vec::with_capacity(BLOCK_SIZE + message.len());
624    inner_msg.extend_from_slice(&ipad);
625    inner_msg.extend_from_slice(message);
626    let inner_hash = sha256(&inner_msg);
627
628    // outer = SHA256(opad || inner_hash)
629    let mut outer_msg = Vec::with_capacity(BLOCK_SIZE + 32);
630    outer_msg.extend_from_slice(&opad);
631    outer_msg.extend_from_slice(&inner_hash);
632    sha256(&outer_msg)
633}
634
635/// Pure Rust SHA-256 implementation
636pub(crate) fn sha256(data: &[u8]) -> [u8; 32] {
637    const K: [u32; 64] = [
638        0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
639        0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
640        0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
641        0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
642        0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
643        0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
644        0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
645        0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
646        0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
647        0xc67178f2,
648    ];
649
650    let mut h: [u32; 8] = [
651        0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
652        0x5be0cd19,
653    ];
654
655    // Pre-processing: padding
656    let bit_len = (data.len() as u64) * 8;
657    let mut padded = data.to_vec();
658    padded.push(0x80);
659    while (padded.len() % 64) != 56 {
660        padded.push(0x00);
661    }
662    padded.extend_from_slice(&bit_len.to_be_bytes());
663
664    // Process each 512-bit (64-byte) block
665    for chunk in padded.chunks_exact(64) {
666        let mut w = [0u32; 64];
667        for i in 0..16 {
668            w[i] = u32::from_be_bytes([
669                chunk[i * 4],
670                chunk[i * 4 + 1],
671                chunk[i * 4 + 2],
672                chunk[i * 4 + 3],
673            ]);
674        }
675        for i in 16..64 {
676            let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
677            let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
678            w[i] = w[i - 16]
679                .wrapping_add(s0)
680                .wrapping_add(w[i - 7])
681                .wrapping_add(s1);
682        }
683
684        let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h;
685
686        for i in 0..64 {
687            let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
688            let ch = (e & f) ^ ((!e) & g);
689            let temp1 = hh
690                .wrapping_add(s1)
691                .wrapping_add(ch)
692                .wrapping_add(K[i])
693                .wrapping_add(w[i]);
694            let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
695            let maj = (a & b) ^ (a & c) ^ (b & c);
696            let temp2 = s0.wrapping_add(maj);
697
698            hh = g;
699            g = f;
700            f = e;
701            e = d.wrapping_add(temp1);
702            d = c;
703            c = b;
704            b = a;
705            a = temp1.wrapping_add(temp2);
706        }
707
708        h[0] = h[0].wrapping_add(a);
709        h[1] = h[1].wrapping_add(b);
710        h[2] = h[2].wrapping_add(c);
711        h[3] = h[3].wrapping_add(d);
712        h[4] = h[4].wrapping_add(e);
713        h[5] = h[5].wrapping_add(f);
714        h[6] = h[6].wrapping_add(g);
715        h[7] = h[7].wrapping_add(hh);
716    }
717
718    let mut result = [0u8; 32];
719    for (i, val) in h.iter().enumerate() {
720        result[i * 4..i * 4 + 4].copy_from_slice(&val.to_be_bytes());
721    }
722    result
723}
724
725/// PBKDF2-HMAC-SHA1 key derivation (pure Rust) for legacy compatibility
726pub fn pbkdf2_hmac_sha1(password: &[u8], salt: &[u8], iterations: u32, key_len: usize) -> Vec<u8> {
727    let mut result = Vec::with_capacity(key_len);
728    let mut block_num = 1u32;
729
730    while result.len() < key_len {
731        let block = pbkdf2_f_sha1(password, salt, iterations, block_num);
732        let needed = key_len - result.len();
733        result.extend_from_slice(&block[..needed.min(20)]);
734        block_num += 1;
735    }
736
737    result.truncate(key_len);
738    result
739}
740
741/// PBKDF2 F function for SHA-1
742fn pbkdf2_f_sha1(password: &[u8], salt: &[u8], iterations: u32, block_num: u32) -> [u8; 20] {
743    let mut msg = Vec::with_capacity(salt.len() + 4);
744    msg.extend_from_slice(salt);
745    msg.extend_from_slice(&block_num.to_be_bytes());
746
747    let mut u_prev = hmac_sha1(password, &msg);
748    let mut result = u_prev;
749
750    for _ in 1..iterations {
751        let u_curr = hmac_sha1(password, &u_prev);
752        for (r, c) in result.iter_mut().zip(u_curr.iter()) {
753            *r ^= c;
754        }
755        u_prev = u_curr;
756    }
757
758    result
759}
760
761/// HMAC-SHA1 (pure Rust)
762fn hmac_sha1(key: &[u8], message: &[u8]) -> [u8; 20] {
763    const BLOCK_SIZE: usize = 64;
764
765    let normalized_key = if key.len() > BLOCK_SIZE {
766        let h = sha1(key);
767        let mut k = [0u8; BLOCK_SIZE];
768        k[..20].copy_from_slice(&h);
769        k
770    } else {
771        let mut k = [0u8; BLOCK_SIZE];
772        k[..key.len()].copy_from_slice(key);
773        k
774    };
775
776    let mut ipad = [0x36u8; BLOCK_SIZE];
777    let mut opad = [0x5cu8; BLOCK_SIZE];
778    for i in 0..BLOCK_SIZE {
779        ipad[i] ^= normalized_key[i];
780        opad[i] ^= normalized_key[i];
781    }
782
783    let mut inner_msg = Vec::with_capacity(BLOCK_SIZE + message.len());
784    inner_msg.extend_from_slice(&ipad);
785    inner_msg.extend_from_slice(message);
786    let inner_hash = sha1(&inner_msg);
787
788    let mut outer_msg = Vec::with_capacity(BLOCK_SIZE + 20);
789    outer_msg.extend_from_slice(&opad);
790    outer_msg.extend_from_slice(&inner_hash);
791    sha1(&outer_msg)
792}
793
794/// Pure Rust SHA-1 implementation
795pub(crate) fn sha1(data: &[u8]) -> [u8; 20] {
796    let mut h0: u32 = 0x67452301;
797    let mut h1: u32 = 0xEFCDAB89;
798    let mut h2: u32 = 0x98BADCFE;
799    let mut h3: u32 = 0x10325476;
800    let mut h4: u32 = 0xC3D2E1F0;
801
802    let bit_len = (data.len() as u64) * 8;
803    let mut padded = data.to_vec();
804    padded.push(0x80);
805    while (padded.len() % 64) != 56 {
806        padded.push(0x00);
807    }
808    padded.extend_from_slice(&bit_len.to_be_bytes());
809
810    for chunk in padded.chunks_exact(64) {
811        let mut w = [0u32; 80];
812        for i in 0..16 {
813            w[i] = u32::from_be_bytes([
814                chunk[i * 4],
815                chunk[i * 4 + 1],
816                chunk[i * 4 + 2],
817                chunk[i * 4 + 3],
818            ]);
819        }
820        for i in 16..80 {
821            w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
822        }
823
824        let (mut a, mut b, mut c, mut d, mut e) = (h0, h1, h2, h3, h4);
825
826        for (i, w_i) in w.iter().enumerate() {
827            let (f_val, k) = match i {
828                0..=19 => ((b & c) | ((!b) & d), 0x5A827999u32),
829                20..=39 => (b ^ c ^ d, 0x6ED9EBA1u32),
830                40..=59 => ((b & c) | (b & d) | (c & d), 0x8F1BBCDCu32),
831                _ => (b ^ c ^ d, 0xCA62C1D6u32),
832            };
833            let temp = a
834                .rotate_left(5)
835                .wrapping_add(f_val)
836                .wrapping_add(e)
837                .wrapping_add(k)
838                .wrapping_add(*w_i);
839            e = d;
840            d = c;
841            c = b.rotate_left(30);
842            b = a;
843            a = temp;
844        }
845
846        h0 = h0.wrapping_add(a);
847        h1 = h1.wrapping_add(b);
848        h2 = h2.wrapping_add(c);
849        h3 = h3.wrapping_add(d);
850        h4 = h4.wrapping_add(e);
851    }
852
853    let mut result = [0u8; 20];
854    result[0..4].copy_from_slice(&h0.to_be_bytes());
855    result[4..8].copy_from_slice(&h1.to_be_bytes());
856    result[8..12].copy_from_slice(&h2.to_be_bytes());
857    result[12..16].copy_from_slice(&h3.to_be_bytes());
858    result[16..20].copy_from_slice(&h4.to_be_bytes());
859    result
860}
861
862/// EVP_BytesToKey using MD5 -- used by legacy OpenSSL encrypted PEM
863///
864/// Derives key material from password + salt using iterated MD5 hashing.
865pub(crate) fn evp_bytes_to_key_md5(password: &[u8], salt: &[u8], key_len: usize) -> Vec<u8> {
866    let mut result = Vec::with_capacity(key_len);
867    let mut d_prev: Option<[u8; 16]> = None;
868
869    while result.len() < key_len {
870        let mut input = Vec::new();
871        if let Some(prev) = d_prev {
872            input.extend_from_slice(&prev);
873        }
874        input.extend_from_slice(password);
875        input.extend_from_slice(&salt[..8.min(salt.len())]);
876        let hash = md5(&input);
877        let needed = key_len - result.len();
878        result.extend_from_slice(&hash[..needed.min(16)]);
879        d_prev = Some(hash);
880    }
881
882    result.truncate(key_len);
883    result
884}
885
886/// Pure Rust MD5 implementation (for EVP_BytesToKey only)
887pub(crate) fn md5(data: &[u8]) -> [u8; 16] {
888    const S: [u32; 64] = [
889        7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5,
890        9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10,
891        15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
892    ];
893    #[allow(clippy::unreadable_literal)]
894    const T: [u32; 64] = [
895        0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613,
896        0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193,
897        0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d,
898        0x02441453, 0xd8a1e681, 0xe7d3fbc8, 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
899        0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122,
900        0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa,
901        0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 0xf4292244,
902        0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
903        0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb,
904        0xeb86d391,
905    ];
906
907    let mut a0: u32 = 0x67452301;
908    let mut b0: u32 = 0xefcdab89;
909    let mut c0: u32 = 0x98badcfe;
910    let mut d0: u32 = 0x10325476;
911
912    let bit_len = (data.len() as u64) * 8;
913    let mut padded = data.to_vec();
914    padded.push(0x80);
915    while (padded.len() % 64) != 56 {
916        padded.push(0x00);
917    }
918    padded.extend_from_slice(&bit_len.to_le_bytes());
919
920    for chunk in padded.chunks_exact(64) {
921        let mut m = [0u32; 16];
922        for i in 0..16 {
923            m[i] = u32::from_le_bytes([
924                chunk[i * 4],
925                chunk[i * 4 + 1],
926                chunk[i * 4 + 2],
927                chunk[i * 4 + 3],
928            ]);
929        }
930
931        let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
932
933        for i in 0..64 {
934            let (f_val, g) = match i {
935                0..=15 => ((b & c) | ((!b) & d), i),
936                16..=31 => ((d & b) | ((!d) & c), (5 * i + 1) % 16),
937                32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
938                _ => (c ^ (b | (!d)), (7 * i) % 16),
939            };
940            let f_val = f_val.wrapping_add(a).wrapping_add(T[i]).wrapping_add(m[g]);
941            a = d;
942            d = c;
943            c = b;
944            b = b.wrapping_add(f_val.rotate_left(S[i]));
945        }
946
947        a0 = a0.wrapping_add(a);
948        b0 = b0.wrapping_add(b);
949        c0 = c0.wrapping_add(c);
950        d0 = d0.wrapping_add(d);
951    }
952
953    let mut result = [0u8; 16];
954    result[0..4].copy_from_slice(&a0.to_le_bytes());
955    result[4..8].copy_from_slice(&b0.to_le_bytes());
956    result[8..12].copy_from_slice(&c0.to_le_bytes());
957    result[12..16].copy_from_slice(&d0.to_le_bytes());
958    result
959}
960
961// ============================================================================
962// Pure Rust AES-CBC decryption
963// ============================================================================
964
965/// AES S-Box
966pub(crate) const AES_SBOX: [u8; 256] = [
967    0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
968    0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
969    0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
970    0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
971    0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
972    0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
973    0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
974    0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
975    0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
976    0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
977    0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
978    0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
979    0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
980    0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
981    0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
982    0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16,
983];
984
985/// Inverse AES S-Box
986const AES_INV_SBOX: [u8; 256] = [
987    0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb,
988    0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb,
989    0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e,
990    0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25,
991    0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92,
992    0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84,
993    0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06,
994    0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b,
995    0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73,
996    0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e,
997    0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b,
998    0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4,
999    0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f,
1000    0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef,
1001    0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61,
1002    0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d,
1003];
1004
1005/// AES round constants
1006const AES_RCON: [u8; 11] = [
1007    0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36,
1008];
1009
1010/// Galois field multiplication by 2
1011fn gf_mul2(a: u8) -> u8 {
1012    if a & 0x80 != 0 {
1013        (a << 1) ^ 0x1b
1014    } else {
1015        a << 1
1016    }
1017}
1018
1019/// Galois field multiplication
1020pub(crate) fn gf_mul(mut a: u8, mut b: u8) -> u8 {
1021    let mut result: u8 = 0;
1022    while b != 0 {
1023        if b & 1 != 0 {
1024            result ^= a;
1025        }
1026        a = gf_mul2(a);
1027        b >>= 1;
1028    }
1029    result
1030}
1031
1032/// AES key expansion
1033pub(crate) fn aes_key_expansion(key: &[u8]) -> NetResult<Vec<[u8; 4]>> {
1034    let nk = key.len() / 4; // Number of 32-bit words in key
1035    let nr = nk + 6; // Number of rounds
1036    let total_words = 4 * (nr + 1);
1037
1038    let mut w: Vec<[u8; 4]> = Vec::with_capacity(total_words);
1039
1040    // First Nk words are the key itself
1041    for i in 0..nk {
1042        w.push([key[4 * i], key[4 * i + 1], key[4 * i + 2], key[4 * i + 3]]);
1043    }
1044
1045    for i in nk..total_words {
1046        let mut temp = w[i - 1];
1047
1048        if i % nk == 0 {
1049            // RotWord
1050            temp = [temp[1], temp[2], temp[3], temp[0]];
1051            // SubWord
1052            temp = [
1053                AES_SBOX[temp[0] as usize],
1054                AES_SBOX[temp[1] as usize],
1055                AES_SBOX[temp[2] as usize],
1056                AES_SBOX[temp[3] as usize],
1057            ];
1058            // XOR with Rcon
1059            let rcon_idx = i / nk;
1060            if rcon_idx >= AES_RCON.len() {
1061                return Err(NetError::InvalidCertificate(
1062                    "AES key expansion: rcon index out of bounds".to_string(),
1063                ));
1064            }
1065            temp[0] ^= AES_RCON[rcon_idx];
1066        } else if nk > 6 && i % nk == 4 {
1067            // For AES-256: SubWord on 4th word
1068            temp = [
1069                AES_SBOX[temp[0] as usize],
1070                AES_SBOX[temp[1] as usize],
1071                AES_SBOX[temp[2] as usize],
1072                AES_SBOX[temp[3] as usize],
1073            ];
1074        }
1075
1076        let prev = w[i - nk];
1077        w.push([
1078            prev[0] ^ temp[0],
1079            prev[1] ^ temp[1],
1080            prev[2] ^ temp[2],
1081            prev[3] ^ temp[3],
1082        ]);
1083    }
1084
1085    Ok(w)
1086}
1087
1088/// AES block decryption (single 16-byte block)
1089fn aes_decrypt_block(block: &[u8; 16], round_keys: &[[u8; 4]]) -> [u8; 16] {
1090    let nr = round_keys.len() / 4 - 1;
1091    let mut state = [[0u8; 4]; 4];
1092
1093    // Copy input to state (column-major order)
1094    for c in 0..4 {
1095        for r in 0..4 {
1096            state[r][c] = block[c * 4 + r];
1097        }
1098    }
1099
1100    // Initial round key addition
1101    for c in 0..4 {
1102        for r in 0..4 {
1103            state[r][c] ^= round_keys[nr * 4 + c][r];
1104        }
1105    }
1106
1107    // Main rounds (in reverse)
1108    for round in (1..nr).rev() {
1109        // InvShiftRows
1110        inv_shift_rows(&mut state);
1111        // InvSubBytes
1112        inv_sub_bytes(&mut state);
1113        // AddRoundKey
1114        for c in 0..4 {
1115            for r in 0..4 {
1116                state[r][c] ^= round_keys[round * 4 + c][r];
1117            }
1118        }
1119        // InvMixColumns
1120        inv_mix_columns(&mut state);
1121    }
1122
1123    // Final round
1124    inv_shift_rows(&mut state);
1125    inv_sub_bytes(&mut state);
1126    for c in 0..4 {
1127        for r in 0..4 {
1128            state[r][c] ^= round_keys[c][r];
1129        }
1130    }
1131
1132    // State to output
1133    let mut output = [0u8; 16];
1134    for c in 0..4 {
1135        for r in 0..4 {
1136            output[c * 4 + r] = state[r][c];
1137        }
1138    }
1139    output
1140}
1141
1142fn inv_sub_bytes(state: &mut [[u8; 4]; 4]) {
1143    for row in state.iter_mut() {
1144        for val in row.iter_mut() {
1145            *val = AES_INV_SBOX[*val as usize];
1146        }
1147    }
1148}
1149
1150fn inv_shift_rows(state: &mut [[u8; 4]; 4]) {
1151    // Row 0: no shift
1152    // Row 1: shift right by 1
1153    let tmp = state[1][3];
1154    state[1][3] = state[1][2];
1155    state[1][2] = state[1][1];
1156    state[1][1] = state[1][0];
1157    state[1][0] = tmp;
1158    // Row 2: shift right by 2
1159    let (t0, t1) = (state[2][0], state[2][1]);
1160    state[2][0] = state[2][2];
1161    state[2][1] = state[2][3];
1162    state[2][2] = t0;
1163    state[2][3] = t1;
1164    // Row 3: shift right by 3
1165    let tmp = state[3][0];
1166    state[3][0] = state[3][1];
1167    state[3][1] = state[3][2];
1168    state[3][2] = state[3][3];
1169    state[3][3] = tmp;
1170}
1171
1172#[allow(clippy::needless_range_loop)]
1173fn inv_mix_columns(state: &mut [[u8; 4]; 4]) {
1174    // Column-major operation: each column c spans all 4 rows, so range loop is clearest
1175    for c in 0..4 {
1176        let s0 = state[0][c];
1177        let s1 = state[1][c];
1178        let s2 = state[2][c];
1179        let s3 = state[3][c];
1180
1181        state[0][c] = gf_mul(s0, 0x0e) ^ gf_mul(s1, 0x0b) ^ gf_mul(s2, 0x0d) ^ gf_mul(s3, 0x09);
1182        state[1][c] = gf_mul(s0, 0x09) ^ gf_mul(s1, 0x0e) ^ gf_mul(s2, 0x0b) ^ gf_mul(s3, 0x0d);
1183        state[2][c] = gf_mul(s0, 0x0d) ^ gf_mul(s1, 0x09) ^ gf_mul(s2, 0x0e) ^ gf_mul(s3, 0x0b);
1184        state[3][c] = gf_mul(s0, 0x0b) ^ gf_mul(s1, 0x0d) ^ gf_mul(s2, 0x09) ^ gf_mul(s3, 0x0e);
1185    }
1186}
1187
1188/// AES-CBC decryption
1189pub(crate) fn aes_cbc_decrypt(ciphertext: &[u8], key: &[u8], iv: &[u8]) -> NetResult<Vec<u8>> {
1190    if ciphertext.len() % 16 != 0 {
1191        return Err(NetError::InvalidCertificate(
1192            "Decryption failed: ciphertext length is not a multiple of 16".to_string(),
1193        ));
1194    }
1195    if iv.len() != 16 {
1196        return Err(NetError::InvalidCertificate(format!(
1197            "Decryption failed: IV must be 16 bytes, got {}",
1198            iv.len()
1199        )));
1200    }
1201    if key.len() != 16 && key.len() != 24 && key.len() != 32 {
1202        return Err(NetError::InvalidCertificate(format!(
1203            "Decryption failed: invalid key length {} (expected 16, 24, or 32)",
1204            key.len()
1205        )));
1206    }
1207
1208    let round_keys = aes_key_expansion(key)?;
1209    let mut result = Vec::with_capacity(ciphertext.len());
1210    let mut prev_cipher_block = [0u8; 16];
1211    prev_cipher_block.copy_from_slice(&iv[..16]);
1212
1213    for chunk in ciphertext.chunks_exact(16) {
1214        let mut block = [0u8; 16];
1215        block.copy_from_slice(chunk);
1216
1217        let decrypted_block = aes_decrypt_block(&block, &round_keys);
1218
1219        // XOR with previous ciphertext block (CBC mode)
1220        for i in 0..16 {
1221            result.push(decrypted_block[i] ^ prev_cipher_block[i]);
1222        }
1223
1224        prev_cipher_block = block;
1225    }
1226
1227    Ok(result)
1228}
1229
1230/// Remove PKCS#7 padding from decrypted data
1231pub(crate) fn remove_pkcs7_padding(data: &[u8]) -> NetResult<&[u8]> {
1232    if data.is_empty() {
1233        return Err(NetError::InvalidCertificate(
1234            "Decryption failed: empty decrypted data".to_string(),
1235        ));
1236    }
1237
1238    let pad_len = *data.last().unwrap_or(&0) as usize;
1239    if pad_len == 0 || pad_len > 16 || pad_len > data.len() {
1240        return Err(NetError::InvalidCertificate(
1241            "Decryption failed: wrong password or corrupted key (invalid PKCS#7 padding)"
1242                .to_string(),
1243        ));
1244    }
1245
1246    // Verify all padding bytes
1247    for &b in &data[data.len() - pad_len..] {
1248        if b as usize != pad_len {
1249            return Err(NetError::InvalidCertificate(
1250                "Decryption failed: wrong password or corrupted key (invalid PKCS#7 padding)"
1251                    .to_string(),
1252            ));
1253        }
1254    }
1255
1256    Ok(&data[..data.len() - pad_len])
1257}
1258
1259// ============================================================================
1260// Hex and Base64 utilities
1261// ============================================================================
1262
1263/// Decode hex string to bytes
1264pub(crate) fn hex_decode(hex: &str) -> Result<Vec<u8>, String> {
1265    if hex.len() % 2 != 0 {
1266        return Err("Hex string has odd length".to_string());
1267    }
1268    let mut bytes = Vec::with_capacity(hex.len() / 2);
1269    let mut chars = hex.chars();
1270    while let (Some(h), Some(l)) = (chars.next(), chars.next()) {
1271        let high = h
1272            .to_digit(16)
1273            .ok_or_else(|| format!("Invalid hex character: {h}"))? as u8;
1274        let low = l
1275            .to_digit(16)
1276            .ok_or_else(|| format!("Invalid hex character: {l}"))? as u8;
1277        bytes.push((high << 4) | low);
1278    }
1279    Ok(bytes)
1280}
1281
1282/// Decode base64 string to bytes (pure Rust)
1283pub(crate) fn base64_decode_pure(input: &str) -> Result<Vec<u8>, String> {
1284    let mut output = Vec::with_capacity(input.len() * 3 / 4);
1285    let mut buf: u32 = 0;
1286    let mut bits: u32 = 0;
1287    let mut pad_count = 0;
1288
1289    for c in input.chars() {
1290        if c == '=' {
1291            pad_count += 1;
1292            continue;
1293        }
1294        if pad_count > 0 {
1295            return Err("Invalid base64: data after padding".to_string());
1296        }
1297        let val = match c {
1298            'A'..='Z' => c as u32 - 'A' as u32,
1299            'a'..='z' => c as u32 - 'a' as u32 + 26,
1300            '0'..='9' => c as u32 - '0' as u32 + 52,
1301            '+' => 62,
1302            '/' => 63,
1303            _ => return Err(format!("Invalid base64 character: {c}")),
1304        };
1305        buf = (buf << 6) | val;
1306        bits += 6;
1307        if bits >= 8 {
1308            bits -= 8;
1309            output.push((buf >> bits) as u8);
1310            buf &= (1 << bits) - 1;
1311        }
1312    }
1313
1314    Ok(output)
1315}