//! Encryption format detection and backward compatibility for token storage //! //! This module provides: //! - Format detection to distinguish between hex-encoded (Wave 154) and AES-GCM encrypted (Wave 155) tokens //! - Encryption/decryption functions using AES-256-GCM authenticated encryption //! - Backward compatibility layer for seamless migration from hex to encrypted format //! //! The detection is based on a simple prefix check: encrypted data starts with "ENC:" use aes_gcm::{ aead::{Aead, KeyInit}, Aes256Gcm, Nonce, }; use base64::{engine::general_purpose, Engine as _}; use common::{error::ErrorCategory, CommonError}; use rand::{rngs::OsRng, RngCore}; /// Encryption format for MFA secrets and tokens #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EncryptionFormat { /// Legacy format: plain hex-encoded secret /// Example: "4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d" HexEncoded, /// New format: AES-GCM encrypted secret with "ENC:" prefix /// Example: "`ENC:base64_encoded_encrypted_data`" AesGcmEncrypted, } impl EncryptionFormat { /// Detect encryption format from file content /// /// # Logic /// - If data starts with "ENC:", it's AES-GCM encrypted /// - Otherwise, it's hex-encoded (legacy format) /// /// # Arguments /// * `data` - The raw file content (trimmed recommended) /// /// # Returns /// The detected encryption format /// /// # Examples /// ``` /// use fxt::auth::encryption::EncryptionFormat; /// /// let hex_data = "4a5b6c7d8e9f0a1b"; /// assert_eq!(EncryptionFormat::detect(hex_data), EncryptionFormat::HexEncoded); /// /// let encrypted_data = "ENC:abc123=="; /// assert_eq!(EncryptionFormat::detect(encrypted_data), EncryptionFormat::AesGcmEncrypted); /// ``` pub fn detect(data: &str) -> Self { if data.starts_with("ENC:") { EncryptionFormat::AesGcmEncrypted } else { EncryptionFormat::HexEncoded } } /// Check if data is encrypted (convenience method) /// /// # Arguments /// * `data` - The raw file content /// /// # Returns /// `true` if data is in encrypted format, `false` if hex-encoded /// /// # Examples /// ``` /// use fxt::auth::encryption::EncryptionFormat; /// /// assert!(!EncryptionFormat::is_encrypted("4a5b6c7d")); /// assert!(EncryptionFormat::is_encrypted("ENC:data")); /// ``` pub fn is_encrypted(data: &str) -> bool { data.starts_with("ENC:") } } /// Encrypt a token using AES-256-GCM authenticated encryption /// /// # Format /// The output format is: "ENC:" + base64(nonce || ciphertext || tag) /// - nonce: 12 bytes (96 bits) - randomly generated /// - ciphertext: variable length (same as plaintext) /// - tag: 16 bytes (128 bits) - authentication tag appended by GCM /// /// # Arguments /// * `token` - The plaintext token to encrypt /// * `key` - The 32-byte (256-bit) AES encryption key /// /// # Returns /// * `Ok(String)` - Encrypted token with "ENC:" prefix and base64-encoded data /// * `Err(CommonError)` - If key length is invalid or encryption fails /// /// # Security /// - Uses cryptographically secure random nonce generation (`OsRng`) /// - GCM mode provides authenticated encryption (confidentiality + integrity) /// - Each encryption uses a unique nonce (never reuse with same key) /// /// # Examples /// ```no_run /// use fxt::auth::encryption::encrypt_token; /// /// let key = [0u8; 32]; // 32-byte key (in production, use proper key derivation) /// let token = "my_secret_token"; /// let encrypted = encrypt_token(token, &key).unwrap(); /// assert!(encrypted.starts_with("ENC:")); /// ``` pub fn encrypt_token(token: &str, key: &[u8]) -> Result { // Validate key length (AES-256 requires exactly 32 bytes) if key.len() != 32 { return Err(CommonError::service( ErrorCategory::Security, format!( "Invalid encryption key length: expected 32 bytes, got {}", key.len() ), )); } // Generate random 12-byte nonce (96 bits, recommended for GCM) let mut nonce_bytes = [0_u8; 12]; OsRng.fill_bytes(&mut nonce_bytes); let nonce = Nonce::from_slice(&nonce_bytes); // Create AES-256-GCM cipher let cipher = Aes256Gcm::new_from_slice(key).map_err(|e| { CommonError::service( ErrorCategory::Security, format!("Failed to create cipher: {}", e), ) })?; // Encrypt the token (GCM automatically appends 16-byte authentication tag) let ciphertext = cipher.encrypt(nonce, token.as_bytes()).map_err(|e| { CommonError::service(ErrorCategory::Security, format!("Encryption failed: {}", e)) })?; // Combine nonce + ciphertext (ciphertext already includes the 16-byte tag) let mut combined = Vec::with_capacity(12 + ciphertext.len()); combined.extend_from_slice(&nonce_bytes); combined.extend_from_slice(&ciphertext); // Encode as base64 with "ENC:" prefix let encoded = general_purpose::STANDARD.encode(&combined); Ok(format!("ENC:{}", encoded)) } /// Decrypt a token using AES-256-GCM authenticated encryption /// /// # Format /// The input must be: "ENC:" + base64(nonce || ciphertext || tag) /// - nonce: 12 bytes (96 bits) /// - ciphertext: variable length (same as original plaintext) /// - tag: 16 bytes (128 bits) - authentication tag verified by GCM /// /// # Arguments /// * `encrypted` - The encrypted token (must start with "ENC:") /// * `key` - The 32-byte (256-bit) AES decryption key /// /// # Returns /// * `Ok(String)` - Decrypted plaintext token /// * `Err(CommonError)` - If prefix missing, key invalid, data corrupted, or authentication fails /// /// # Security /// - GCM automatically verifies authentication tag before decryption /// - Wrong key or tampered data will fail authentication /// - Prevents forgery and modification attacks /// /// # Errors /// - Missing "ENC:" prefix /// - Invalid key length (not 32 bytes) /// - Base64 decode failure /// - Data too short (< 28 bytes: 12 nonce + 16 tag minimum) /// - Decryption failure (wrong key or corrupted data) /// - Authentication tag verification failure /// - UTF-8 decode failure /// /// # Examples /// ```no_run /// use fxt::auth::encryption::{encrypt_token, decrypt_token}; /// /// let key = [0u8; 32]; /// let token = "my_secret_token"; /// let encrypted = encrypt_token(token, &key).unwrap(); /// let decrypted = decrypt_token(&encrypted, &key).unwrap(); /// assert_eq!(decrypted, token); /// ``` pub fn decrypt_token(encrypted: &str, key: &[u8]) -> Result { // Validate key length (AES-256 requires exactly 32 bytes) if key.len() != 32 { return Err(CommonError::service( ErrorCategory::Security, format!("Invalid key length: expected 32 bytes, got {}", key.len()), )); } // Validate "ENC:" prefix if !encrypted.starts_with("ENC:") { return Err(CommonError::service( ErrorCategory::Security, "Missing ENC: prefix".to_owned(), )); } // Strip "ENC:" prefix and decode base64 let base64_data = encrypted.get(4..).ok_or_else(|| { CommonError::service( ErrorCategory::Security, "Encrypted data too short for ENC: prefix".to_owned(), ) })?; let combined = general_purpose::STANDARD.decode(base64_data).map_err(|e| { CommonError::service( ErrorCategory::Security, format!("Base64 decode failed: {}", e), ) })?; // Validate minimum length: 12 bytes (nonce) + 16 bytes (tag) = 28 bytes if combined.len() < 28 { return Err(CommonError::service( ErrorCategory::Security, format!( "Data too short: expected at least 28 bytes, got {}", combined.len() ), )); } // Extract nonce (first 12 bytes) -- length validated above (>= 28). let nonce_bytes = combined.get(..12).ok_or_else(|| { CommonError::service(ErrorCategory::Security, "Nonce extraction failed".to_owned()) })?; let nonce = Nonce::from_slice(nonce_bytes); // Extract ciphertext + tag (remaining bytes, tag is last 16 bytes included in ciphertext) let ciphertext = combined.get(12..).ok_or_else(|| { CommonError::service(ErrorCategory::Security, "Ciphertext extraction failed".to_owned()) })?; // Create AES-256-GCM cipher let cipher = Aes256Gcm::new_from_slice(key).map_err(|e| { CommonError::service( ErrorCategory::Security, format!("Cipher creation failed: {}", e), ) })?; // Decrypt and verify authentication tag (GCM does both automatically) let plaintext_bytes = cipher.decrypt(nonce, ciphertext).map_err(|e| { CommonError::service(ErrorCategory::Security, format!("Decryption failed: {}", e)) })?; // Convert decrypted bytes to UTF-8 string let plaintext = String::from_utf8(plaintext_bytes).map_err(|e| { CommonError::service( ErrorCategory::Security, format!("UTF-8 decode failed: {}", e), ) })?; Ok(plaintext) } /// Auto-read token with format detection (backward compatibility) /// /// This function automatically detects whether the stored token is: /// - Legacy hex-encoded format (Wave 154) /// - New AES-GCM encrypted format (Wave 155) /// /// And returns the plaintext token in both cases. /// /// # Arguments /// * `encrypted_data` - Either hex-encoded or "ENC:" prefixed encrypted data /// * `key` - 32-byte decryption key (only used for encrypted format) /// /// # Returns /// * `Ok(String)` - The plaintext token /// * `Err(CommonError)` - If decoding/decryption fails /// /// # Migration Strategy /// This function enables seamless migration: /// - Read: Supports both hex (old) and encrypted (new) formats /// - Write: Use `write_token_encrypted()` to always write encrypted format /// - Result: Automatic migration on first token refresh /// /// # Examples /// ```no_run /// use fxt::auth::encryption::read_token_auto; /// /// let key = [0u8; 32]; /// /// // Read legacy hex-encoded token (Wave 154) /// let hex_token = "6d795f746f6b656e"; // "my_token" in hex /// let plaintext = read_token_auto(hex_token, &key).unwrap(); /// assert_eq!(plaintext, "my_token"); /// /// // Read new encrypted token (Wave 155) /// let encrypted = "ENC:base64data"; /// let plaintext = read_token_auto(encrypted, &key).unwrap(); /// ``` pub fn read_token_auto(encrypted_data: &str, key: &[u8]) -> Result { match EncryptionFormat::detect(encrypted_data) { EncryptionFormat::HexEncoded => { // Legacy format: decode hex and return plaintext let token_bytes = hex::decode(encrypted_data).map_err(|e| { CommonError::service( ErrorCategory::Security, format!( "Failed to decode hex-encoded token (backward compatibility): {}", e ), ) })?; String::from_utf8(token_bytes).map_err(|e| { CommonError::service( ErrorCategory::Security, format!("Hex-decoded token is not valid UTF-8: {}", e), ) }) }, EncryptionFormat::AesGcmEncrypted => { // New format: decrypt using AES-GCM decrypt_token(encrypted_data, key) }, } } /// Always write token in encrypted format (migration helper) /// /// This function enforces the migration strategy: all new writes use encrypted format. /// When combined with `read_token_auto()`, this enables seamless migration: /// 1. First read after Wave 155: `read_token_auto()` handles old hex format /// 2. First write after Wave 155: `write_token_encrypted()` converts to encrypted format /// 3. Subsequent operations: encrypted format throughout /// /// # Arguments /// * `token` - The plaintext token to encrypt and store /// * `key` - 32-byte encryption key /// /// # Returns /// * `Ok(String)` - Encrypted data in format: "ENC:base64(nonce || ciphertext || tag)" /// * `Err(CommonError)` - If encryption fails /// /// # Examples /// ```no_run /// use fxt::auth::encryption::{read_token_auto, write_token_encrypted}; /// /// let key = [0u8; 32]; /// /// // Migration scenario: /// // 1. Read old hex-encoded token /// let hex_token = "6d795f746f6b656e"; // "my_token" in hex /// let plaintext = read_token_auto(hex_token, &key).unwrap(); /// /// // 2. Write in new encrypted format (migration happens here) /// let encrypted = write_token_encrypted(&plaintext, &key).unwrap(); /// assert!(encrypted.starts_with("ENC:")); /// /// // 3. Future reads will use encrypted format /// let plaintext2 = read_token_auto(&encrypted, &key).unwrap(); /// assert_eq!(plaintext, plaintext2); /// ``` pub fn write_token_encrypted(token: &str, key: &[u8]) -> Result { // Always use encrypted format for new writes encrypt_token(token, key) } #[cfg(test)] #[allow(clippy::indexing_slicing, clippy::string_slice, clippy::unwrap_used, clippy::expect_used, clippy::shadow_unrelated, clippy::shadow_reuse, clippy::assertions_on_result_states, clippy::str_to_string)] mod tests { use super::*; #[test] fn test_detect_hex_format() { // Typical hex-encoded MFA secret (32 chars = 16 bytes) let hex_secret = "4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d"; assert_eq!( EncryptionFormat::detect(hex_secret), EncryptionFormat::HexEncoded ); // Short hex string let short_hex = "abc123"; assert_eq!( EncryptionFormat::detect(short_hex), EncryptionFormat::HexEncoded ); // Empty string (edge case - treated as hex) let empty = ""; assert_eq!( EncryptionFormat::detect(empty), EncryptionFormat::HexEncoded ); // Hex string with whitespace (should be trimmed before detection) let hex_with_whitespace = " 4a5b6c7d8e9f0a1b "; assert_eq!( EncryptionFormat::detect(hex_with_whitespace.trim()), EncryptionFormat::HexEncoded ); } #[test] fn test_detect_encrypted_format() { // Typical encrypted format with base64 data let encrypted = "ENC:YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo="; assert_eq!( EncryptionFormat::detect(encrypted), EncryptionFormat::AesGcmEncrypted ); // Minimal encrypted format (just prefix) let minimal = "ENC:"; assert_eq!( EncryptionFormat::detect(minimal), EncryptionFormat::AesGcmEncrypted ); // Encrypted format with short data let short_encrypted = "ENC:abc"; assert_eq!( EncryptionFormat::detect(short_encrypted), EncryptionFormat::AesGcmEncrypted ); // Encrypted format with whitespace after prefix let encrypted_with_space = "ENC: data"; assert_eq!( EncryptionFormat::detect(encrypted_with_space), EncryptionFormat::AesGcmEncrypted ); } #[test] fn test_is_encrypted() { // Hex-encoded (not encrypted) assert!(!EncryptionFormat::is_encrypted("4a5b6c7d8e9f0a1b")); assert!(!EncryptionFormat::is_encrypted("")); assert!(!EncryptionFormat::is_encrypted("random_string")); // Encrypted format assert!(EncryptionFormat::is_encrypted("ENC:data")); assert!(EncryptionFormat::is_encrypted("ENC:")); assert!(EncryptionFormat::is_encrypted( "ENC:YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=" )); } #[test] fn test_edge_cases() { // String starting with "enc:" (lowercase) - treated as hex let lowercase = "enc:data"; assert_eq!( EncryptionFormat::detect(lowercase), EncryptionFormat::HexEncoded ); assert!(!EncryptionFormat::is_encrypted(lowercase)); // String containing "ENC:" but not at start - treated as hex let middle = "data_ENC:something"; assert_eq!( EncryptionFormat::detect(middle), EncryptionFormat::HexEncoded ); assert!(!EncryptionFormat::is_encrypted(middle)); // Mixed case prefix - treated as hex let mixed_case = "EnC:data"; assert_eq!( EncryptionFormat::detect(mixed_case), EncryptionFormat::HexEncoded ); assert!(!EncryptionFormat::is_encrypted(mixed_case)); } #[test] fn test_consistency_between_methods() { // Verify detect() and is_encrypted() are consistent let test_cases = vec![ ("4a5b6c7d", false), ("ENC:data", true), ("", false), ("ENC:", true), ("enc:data", false), ("random", false), ]; for (data, expected_encrypted) in test_cases { let detected_format = EncryptionFormat::detect(data); let is_encrypted = EncryptionFormat::is_encrypted(data); // Verify consistency assert_eq!( is_encrypted, expected_encrypted, "is_encrypted mismatch for: {}", data ); assert_eq!( detected_format, if expected_encrypted { EncryptionFormat::AesGcmEncrypted } else { EncryptionFormat::HexEncoded }, "detect mismatch for: {}", data ); } } // Tests for encrypt_token() #[test] fn test_encrypt_token_success() { // Test successful encryption with valid 32-byte key let key = [0_u8; 32]; let token = "my_test_token_12345"; let result = encrypt_token(token, &key); assert!(result.is_ok(), "Encryption should succeed with valid key"); let encrypted = result.unwrap(); // Verify format assert!( encrypted.starts_with("ENC:"), "Encrypted token should start with 'ENC:' prefix" ); // Verify it's detected as encrypted assert!( EncryptionFormat::is_encrypted(&encrypted), "Output should be detected as encrypted format" ); // Verify length (4 + base64_len(12 + token_len + 16)) // Base64 expands data by ~4/3 // Data size: 12 (nonce) + token.len() + 16 (tag) let data_size = 12 + token.len() + 16; let base64_len = data_size.div_ceil(3) * 4; // Base64 length calculation let expected_len = 4 + base64_len; // "ENC:" + base64 assert_eq!( encrypted.len(), expected_len, "Encrypted token length should match expected format" ); } #[test] fn test_encrypt_token_base64_decodable() { // Test that output is valid base64 let key = [1_u8; 32]; let token = "test_token"; let encrypted = encrypt_token(token, &key).unwrap(); // Remove "ENC:" prefix and decode base64 let base64_data = &encrypted[4..]; let decoded = general_purpose::STANDARD.decode(base64_data); assert!(decoded.is_ok(), "Encrypted data should be valid base64"); let decoded_bytes = decoded.unwrap(); // Verify decoded length (12 nonce + token_len + 16 tag) let expected_len = 12 + token.len() + 16; assert_eq!( decoded_bytes.len(), expected_len, "Decoded data should have correct length" ); } #[test] fn test_encrypt_token_invalid_key_length() { // Test encryption fails with wrong key length let token = "test_token"; // Test various invalid key lengths let invalid_keys = vec![ vec![0_u8; 16], // Too short (AES-128) vec![0_u8; 24], // Too short (AES-192) vec![0_u8; 31], // One byte short vec![0_u8; 33], // One byte too long vec![0_u8; 0], // Empty vec![0_u8; 64], // Too long ]; for key in invalid_keys { let result = encrypt_token(token, &key); assert!( result.is_err(), "Encryption should fail with key length {}", key.len() ); // Verify error message mentions key length let err = result.unwrap_err(); let err_msg = format!("{}", err); assert!( err_msg.contains("key length") || err_msg.contains("32 bytes"), "Error should mention key length issue: {}", err_msg ); } } #[test] fn test_encrypt_token_different_outputs() { // Test that same input produces different outputs (due to random nonce) let key = [2_u8; 32]; let token = "same_token"; let encrypted1 = encrypt_token(token, &key).unwrap(); let encrypted2 = encrypt_token(token, &key).unwrap(); // Both should have "ENC:" prefix assert!(encrypted1.starts_with("ENC:")); assert!(encrypted2.starts_with("ENC:")); // But the encrypted data should be different (different nonces) assert_ne!( encrypted1, encrypted2, "Multiple encryptions of same data should produce different outputs" ); } #[test] fn test_encrypt_token_empty_string() { // Test encrypting empty string (edge case) let key = [3_u8; 32]; let token = ""; let result = encrypt_token(token, &key); assert!(result.is_ok(), "Should be able to encrypt empty string"); let encrypted = result.unwrap(); assert!(encrypted.starts_with("ENC:")); // Length should be: 4 + base64_len(12 + 0 + 16) = 4 + base64_len(28) // Base64 of 28 bytes = 38 chars (28 * 4/3 rounded up to multiple of 4) assert_eq!(encrypted.len(), 4 + 40); // "ENC:" + base64(28 bytes) } #[test] fn test_encrypt_token_long_string() { // Test encrypting long string let key = [4_u8; 32]; let token = "a".repeat(1000); // 1000-character token let result = encrypt_token(&token, &key); assert!(result.is_ok(), "Should be able to encrypt long strings"); let encrypted = result.unwrap(); assert!(encrypted.starts_with("ENC:")); // Verify length calculation let data_size = 12 + token.len() + 16; // nonce + plaintext + tag let base64_len = data_size.div_ceil(3) * 4; assert_eq!(encrypted.len(), 4 + base64_len); } #[test] fn test_encrypt_token_special_characters() { // Test encrypting string with special characters let key = [5_u8; 32]; let tokens = vec![ "token-with-dashes", "token_with_underscores", "token.with.dots", "token@with#special$chars%", "token with spaces", "token\nwith\nnewlines", "token\twith\ttabs", "token\u{1f680}with\u{1f600}emojis", ]; for token in tokens { let result = encrypt_token(token, &key); assert!( result.is_ok(), "Should encrypt token with special chars: {:?}", token ); let encrypted = result.unwrap(); assert!(encrypted.starts_with("ENC:")); } } // Tests for decrypt_token() #[test] fn test_decrypt_token_success() { // Test successful decryption with valid key let key = [0_u8; 32]; let token = "test_token_to_decrypt"; // Encrypt first let encrypted = encrypt_token(token, &key).unwrap(); // Decrypt let decrypted = decrypt_token(&encrypted, &key); assert!(decrypted.is_ok(), "Decryption should succeed"); let decrypted_token = decrypted.unwrap(); assert_eq!(decrypted_token, token, "Decrypted should match original"); } #[test] fn test_decrypt_token_missing_prefix() { // Test that decryption fails without "ENC:" prefix let key = [0_u8; 32]; let invalid_data = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo="; let result = decrypt_token(invalid_data, &key); assert!(result.is_err(), "Should fail without ENC: prefix"); let err_msg = format!("{}", result.unwrap_err()); assert!( err_msg.contains("ENC:") || err_msg.contains("prefix"), "Error should mention missing prefix: {}", err_msg ); } #[test] fn test_decrypt_token_invalid_base64() { // Test that decryption fails with invalid base64 let key = [0_u8; 32]; let invalid_data = "ENC:not_valid_base64!!!"; let result = decrypt_token(invalid_data, &key); assert!(result.is_err(), "Should fail with invalid base64"); } #[test] fn test_decrypt_token_data_too_short() { // Test that decryption fails with data too short let key = [0_u8; 32]; // Valid base64 but data too short (less than 28 bytes) let short_data = general_purpose::STANDARD.encode([0_u8; 10]); let invalid_data = format!("ENC:{}", short_data); let result = decrypt_token(&invalid_data, &key); assert!(result.is_err(), "Should fail with data too short"); let err_msg = format!("{}", result.unwrap_err()); assert!( err_msg.contains("too short") || err_msg.contains("28 bytes"), "Error should mention data too short: {}", err_msg ); } #[test] fn test_decrypt_token_wrong_key() { // Test that decryption fails with wrong key let key1 = [0_u8; 32]; let key2 = [1_u8; 32]; let token = "test_token"; // Encrypt with key1 let encrypted = encrypt_token(token, &key1).unwrap(); // Try to decrypt with key2 let result = decrypt_token(&encrypted, &key2); assert!( result.is_err(), "Should fail when decrypting with wrong key" ); } #[test] fn test_decrypt_token_tampered_data() { // Test that decryption fails with tampered data let key = [0_u8; 32]; let token = "test_token"; // Encrypt let encrypted = encrypt_token(token, &key).unwrap(); // Tamper with multiple positions in the base64 payload to guarantee // AES-GCM authentication failure regardless of the random nonce. let mut chars: Vec = encrypted.chars().collect(); for &pos in &[6, 12, 18, 24] { if let Some(c) = chars.get_mut(pos) { *c = if *c == 'A' { 'B' } else { 'A' }; } } let tampered: String = chars.into_iter().collect(); // Try to decrypt tampered data let result = decrypt_token(&tampered, &key); // Should fail (either base64 decode or authentication failure) assert!(result.is_err(), "Should fail when decrypting tampered data"); } // Tests for read_token_auto() (backward compatibility) #[test] fn test_read_token_auto_hex_format() { // Test reading legacy hex-encoded token (Wave 154 format) let key = [0_u8; 32]; let token = "my_token"; let hex_encoded = hex::encode(token); let result = read_token_auto(&hex_encoded, &key); assert!( result.is_ok(), "Should read hex-encoded token (backward compatibility)" ); let plaintext = result.unwrap(); assert_eq!(plaintext, token, "Decoded token should match original"); } #[test] fn test_read_token_auto_encrypted_format() { // Test reading new AES-GCM encrypted token (Wave 155 format) let key = [0_u8; 32]; let token = "my_token"; // Encrypt using Wave 155 format let encrypted = encrypt_token(token, &key).unwrap(); // Read using auto-detection let result = read_token_auto(&encrypted, &key); assert!(result.is_ok(), "Should read encrypted token"); let plaintext = result.unwrap(); assert_eq!(plaintext, token, "Decrypted token should match original"); } #[test] fn test_read_token_auto_format_detection() { // Test that auto-detection works correctly for both formats let key = [0_u8; 32]; let token = "test_token"; // Wave 154 format (hex) let hex_encoded = hex::encode(token); let hex_format = EncryptionFormat::detect(&hex_encoded); assert_eq!( hex_format, EncryptionFormat::HexEncoded, "Should detect hex format" ); // Wave 155 format (encrypted) let encrypted = encrypt_token(token, &key).unwrap(); let enc_format = EncryptionFormat::detect(&encrypted); assert_eq!( enc_format, EncryptionFormat::AesGcmEncrypted, "Should detect encrypted format" ); } #[test] fn test_read_token_auto_invalid_hex() { // Test error handling for invalid hex encoding let key = [0_u8; 32]; let invalid_hex = "not_valid_hex!!!"; let result = read_token_auto(invalid_hex, &key); assert!( result.is_err(), "Should fail with invalid hex (backward compatibility)" ); let err_msg = format!("{}", result.unwrap_err()); assert!( err_msg.contains("hex") || err_msg.contains("backward compatibility"), "Error should mention hex decoding: {}", err_msg ); } #[test] fn test_read_token_auto_invalid_encrypted() { // Test error handling for invalid encrypted format let key = [0_u8; 32]; let invalid_encrypted = "ENC:not_valid_base64!!!"; let result = read_token_auto(invalid_encrypted, &key); assert!(result.is_err(), "Should fail with invalid encrypted data"); } // Tests for write_token_encrypted() (migration helper) #[test] fn test_write_token_encrypted_always_encrypted() { // Test that write_token_encrypted() always returns encrypted format let key = [0_u8; 32]; let token = "my_token"; let result = write_token_encrypted(token, &key); assert!(result.is_ok(), "Should write token in encrypted format"); let encrypted = result.unwrap(); // Verify format assert!( encrypted.starts_with("ENC:"), "Output should always have ENC: prefix" ); // Verify detection assert!( EncryptionFormat::is_encrypted(&encrypted), "Output should be detected as encrypted" ); } #[test] fn test_write_token_encrypted_roundtrip() { // Test that write → read roundtrip works let key = [0_u8; 32]; let token = "roundtrip_token"; // Write (encrypt) let encrypted = write_token_encrypted(token, &key).unwrap(); // Read (decrypt) let decrypted = read_token_auto(&encrypted, &key).unwrap(); assert_eq!(decrypted, token, "Roundtrip should preserve token"); } // Tests for migration scenario #[test] fn test_migration_scenario() { // Test complete migration scenario: hex → auto-read → encrypted write let key = [0_u8; 32]; let token = "migration_token"; // Step 1: Start with Wave 154 hex-encoded token let hex_encoded = hex::encode(token); assert!( !EncryptionFormat::is_encrypted(&hex_encoded), "Initial format should be hex (Wave 154)" ); // Step 2: Read old format using auto-detection let plaintext = read_token_auto(&hex_encoded, &key).unwrap(); assert_eq!(plaintext, token, "Should read hex format correctly"); // Step 3: Write in new encrypted format (migration happens here) let encrypted = write_token_encrypted(&plaintext, &key).unwrap(); assert!( encrypted.starts_with("ENC:"), "New format should be encrypted (Wave 155)" ); // Step 4: Future reads use encrypted format let plaintext2 = read_token_auto(&encrypted, &key).unwrap(); assert_eq!(plaintext2, token, "Should read encrypted format correctly"); // Verify migration completed assert_ne!( hex_encoded, encrypted, "Format should have changed from hex to encrypted" ); } #[test] fn test_migration_multiple_tokens() { // Test migration with multiple different tokens let key = [0_u8; 32]; let tokens = vec![ "token1", "token2_with_underscores", "token-3-with-dashes", "token.4.with.dots", ]; for token in tokens { // Old format (hex) let hex_encoded = hex::encode(token); // Read old format let plaintext = read_token_auto(&hex_encoded, &key).unwrap(); assert_eq!(plaintext, token); // Write new format let encrypted = write_token_encrypted(&plaintext, &key).unwrap(); assert!(encrypted.starts_with("ENC:")); // Read new format let plaintext2 = read_token_auto(&encrypted, &key).unwrap(); assert_eq!(plaintext2, token); } } #[test] fn test_migration_idempotent() { // Test that migrating already-migrated tokens doesn't break let key = [0_u8; 32]; let token = "already_migrated_token"; // Start with encrypted format (already migrated) let encrypted1 = write_token_encrypted(token, &key).unwrap(); // Read and write again (should still work) let plaintext = read_token_auto(&encrypted1, &key).unwrap(); let encrypted2 = write_token_encrypted(&plaintext, &key).unwrap(); // Both encrypted versions should decrypt to same token let decrypted1 = read_token_auto(&encrypted1, &key).unwrap(); let decrypted2 = read_token_auto(&encrypted2, &key).unwrap(); assert_eq!(decrypted1, token); assert_eq!(decrypted2, token); // But ciphertexts will differ (different nonces) assert_ne!( encrypted1, encrypted2, "Different encryptions should produce different ciphertexts" ); } }