diff --git a/libs/sharenet-passport/src/application/use_cases.rs b/libs/sharenet-passport/src/application/use_cases.rs index 8e15e38..d55da23 100644 --- a/libs/sharenet-passport/src/application/use_cases.rs +++ b/libs/sharenet-passport/src/application/use_cases.rs @@ -80,6 +80,7 @@ where &passport.did, &passport.univ_id, &passport.user_profiles, + &passport.date_of_birth, ) .map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?; @@ -169,6 +170,7 @@ where &passport.did, &passport.univ_id, &passport.user_profiles, + &passport.date_of_birth, ) .map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?; @@ -217,7 +219,7 @@ where .map_err(|e| ApplicationError::UseCaseError(format!("Failed to load file: {}", e.into())))?; // Decrypt file - let (seed, public_key, private_key, user_profiles) = self + let (seed, public_key, private_key, user_profiles, date_of_birth) = self .file_encryptor .decrypt(&passport_file, password) .map_err(|e| ApplicationError::UseCaseError(format!("Failed to decrypt file: {}", e.into())))?; @@ -231,6 +233,7 @@ where passport_file.univ_id.clone(), ); passport.user_profiles = user_profiles; + passport.date_of_birth = date_of_birth; // Re-encrypt and save if output path provided if let Some(output_path) = output_path { @@ -243,6 +246,7 @@ where &passport.did, &passport.univ_id, &passport.user_profiles, + &passport.date_of_birth, ) .map_err(|e| ApplicationError::UseCaseError(format!("Failed to re-encrypt file: {}", e.into())))?; @@ -291,6 +295,7 @@ where &passport.did, &passport.univ_id, &passport.user_profiles, + &passport.date_of_birth, ) .map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?; @@ -377,6 +382,7 @@ where &passport.did, &passport.univ_id, &passport.user_profiles, + &passport.date_of_birth, ) .map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?; @@ -451,6 +457,7 @@ where &passport.did, &passport.univ_id, &passport.user_profiles, + &passport.date_of_birth, ) .map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?; @@ -506,6 +513,7 @@ where &passport.did, &passport.univ_id, &passport.user_profiles, + &passport.date_of_birth, ) .map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?; diff --git a/libs/sharenet-passport/src/application/use_cases_test.rs b/libs/sharenet-passport/src/application/use_cases_test.rs index e85ab24..98afba7 100644 --- a/libs/sharenet-passport/src/application/use_cases_test.rs +++ b/libs/sharenet-passport/src/application/use_cases_test.rs @@ -46,6 +46,7 @@ mod tests { language: Some("en".to_string()), notifications_enabled: true, auto_sync: false, + show_date_of_birth: false, }; let result = create_profile_use_case.execute( @@ -104,6 +105,7 @@ mod tests { language: Some("en".to_string()), notifications_enabled: true, auto_sync: false, + show_date_of_birth: false, }; let result1 = create_profile_use_case.execute( @@ -133,6 +135,7 @@ mod tests { language: Some("es".to_string()), notifications_enabled: false, auto_sync: true, + show_date_of_birth: false, }; let result2 = create_profile_use_case.execute( @@ -185,6 +188,7 @@ mod tests { language: Some("en".to_string()), notifications_enabled: true, auto_sync: false, + show_date_of_birth: false, }; create_profile_use_case.execute( @@ -223,6 +227,7 @@ mod tests { language: Some("es".to_string()), notifications_enabled: false, auto_sync: true, + show_date_of_birth: false, }; let result = update_profile_use_case.execute( @@ -282,6 +287,7 @@ mod tests { language: Some("en".to_string()), notifications_enabled: true, auto_sync: false, + show_date_of_birth: false, }; let result = update_profile_use_case.execute( @@ -334,6 +340,7 @@ mod tests { language: Some("en".to_string()), notifications_enabled: true, auto_sync: false, + show_date_of_birth: false, }; create_profile_use_case.execute( diff --git a/libs/sharenet-passport/src/domain/entities.rs b/libs/sharenet-passport/src/domain/entities.rs index 94dd286..8964b64 100644 --- a/libs/sharenet-passport/src/domain/entities.rs +++ b/libs/sharenet-passport/src/domain/entities.rs @@ -80,6 +80,7 @@ pub struct Passport { pub did: Did, pub univ_id: String, pub user_profiles: Vec, + pub date_of_birth: Option, } impl Passport { @@ -108,6 +109,7 @@ impl Passport { language: None, notifications_enabled: true, auto_sync: true, + show_date_of_birth: false, }, ); @@ -118,6 +120,7 @@ impl Passport { did, univ_id, user_profiles: vec![default_profile], + date_of_birth: None, } } @@ -250,6 +253,14 @@ pub struct UserPreferences { pub language: Option, pub notifications_enabled: bool, pub auto_sync: bool, + pub show_date_of_birth: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DateOfBirth { + pub month: u8, + pub day: u8, + pub year: u16, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -298,4 +309,5 @@ pub struct PassportFile { pub created_at: u64, pub version: String, pub enc_user_profiles: Vec, // Encrypted CBOR of Vec + pub enc_date_of_birth: Vec, // Encrypted CBOR of Option } \ No newline at end of file diff --git a/libs/sharenet-passport/src/domain/entities_test.rs b/libs/sharenet-passport/src/domain/entities_test.rs index f933477..2d20ed2 100644 --- a/libs/sharenet-passport/src/domain/entities_test.rs +++ b/libs/sharenet-passport/src/domain/entities_test.rs @@ -75,6 +75,7 @@ mod tests { language: Some("en".to_string()), notifications_enabled: true, auto_sync: false, + show_date_of_birth: false, }; let profile = UserProfile { @@ -120,6 +121,7 @@ mod tests { language: None, notifications_enabled: true, auto_sync: true, + show_date_of_birth: false, }, created_at: 1234567890, updated_at: 1234567890, @@ -162,6 +164,7 @@ mod tests { language: None, notifications_enabled: false, auto_sync: true, + show_date_of_birth: false, }, created_at: 1234567890, updated_at: 1234567890, @@ -195,6 +198,7 @@ mod tests { language: None, notifications_enabled: true, auto_sync: false, + show_date_of_birth: false, }, created_at: 1234567890, updated_at: 1234567890, @@ -223,6 +227,7 @@ mod tests { language: None, notifications_enabled: true, auto_sync: false, + show_date_of_birth: false, }, created_at: 1234567890, updated_at: 1234567890, diff --git a/libs/sharenet-passport/src/domain/traits.rs b/libs/sharenet-passport/src/domain/traits.rs index 20d2725..fcc5bb5 100644 --- a/libs/sharenet-passport/src/domain/traits.rs +++ b/libs/sharenet-passport/src/domain/traits.rs @@ -26,13 +26,14 @@ pub trait FileEncryptor { did: &Did, univ_id: &str, user_profiles: &[UserProfile], + date_of_birth: &Option, ) -> Result; fn decrypt( &self, file: &PassportFile, password: &str, - ) -> Result<(Seed, PublicKey, PrivateKey, Vec), Self::Error>; + ) -> Result<(Seed, PublicKey, PrivateKey, Vec, Option), Self::Error>; } pub trait FileStorage { diff --git a/libs/sharenet-passport/src/infrastructure/crypto/native.rs b/libs/sharenet-passport/src/infrastructure/crypto/native.rs index 6a607d0..1411b3d 100644 --- a/libs/sharenet-passport/src/infrastructure/crypto/native.rs +++ b/libs/sharenet-passport/src/infrastructure/crypto/native.rs @@ -90,6 +90,7 @@ impl FileEncryptor for XChaCha20FileEncryptor { did: &Did, univ_id: &str, user_profiles: &[UserProfile], + date_of_birth: &Option, ) -> Result { // Generate salt and nonce let mut salt = [0u8; SALT_LENGTH]; @@ -118,6 +119,13 @@ impl FileEncryptor for XChaCha20FileEncryptor { .encrypt(&nonce, &*user_profiles_bytes) .map_err(|e| DomainError::CryptographicError(format!("User profiles encryption failed: {}", e)))?; + // Serialize and encrypt date of birth + let date_of_birth_bytes = serde_cbor::to_vec(&date_of_birth) + .map_err(|e| DomainError::CryptographicError(format!("Failed to serialize date of birth: {}", e)))?; + let enc_date_of_birth = cipher + .encrypt(&nonce, &*date_of_birth_bytes) + .map_err(|e| DomainError::CryptographicError(format!("Date of birth encryption failed: {}", e)))?; + // Get current timestamp let created_at = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -136,6 +144,7 @@ impl FileEncryptor for XChaCha20FileEncryptor { created_at, version: "1.0.0".to_string(), enc_user_profiles, + enc_date_of_birth, }) } @@ -143,7 +152,7 @@ impl FileEncryptor for XChaCha20FileEncryptor { &self, file: &PassportFile, password: &str, - ) -> Result<(Seed, PublicKey, PrivateKey, Vec), Self::Error> { + ) -> Result<(Seed, PublicKey, PrivateKey, Vec, Option), Self::Error> { // Validate file format validate_file_format(&file.kdf, &file.cipher)?; @@ -180,7 +189,14 @@ impl FileEncryptor for XChaCha20FileEncryptor { let user_profiles: Vec = serde_cbor::from_slice(&user_profiles_bytes) .map_err(|e| DomainError::CryptographicError(format!("Failed to deserialize user profiles: {}", e)))?; + // Decrypt date of birth + let date_of_birth_bytes = cipher + .decrypt(&nonce, &*file.enc_date_of_birth) + .map_err(|e| DomainError::CryptographicError(format!("Date of birth decryption failed: {}", e)))?; + let date_of_birth: Option = serde_cbor::from_slice(&date_of_birth_bytes) + .map_err(|e| DomainError::CryptographicError(format!("Failed to deserialize date of birth: {}", e)))?; + // Note: univ_id is stored in the PassportFile and will be used when creating the Passport - Ok((seed, public_key, private_key, user_profiles)) + Ok((seed, public_key, private_key, user_profiles, date_of_birth)) } } \ No newline at end of file diff --git a/libs/sharenet-passport/src/infrastructure/crypto/native_test.rs b/libs/sharenet-passport/src/infrastructure/crypto/native_test.rs index 1df46d0..043d57c 100644 --- a/libs/sharenet-passport/src/infrastructure/crypto/native_test.rs +++ b/libs/sharenet-passport/src/infrastructure/crypto/native_test.rs @@ -45,7 +45,7 @@ mod tests { let password = "test-password"; // Encrypt - let encrypted_file = encryptor.encrypt(&seed, password, &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[]).unwrap(); + let encrypted_file = encryptor.encrypt(&seed, password, &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[], &None).unwrap(); // Verify file structure assert_eq!(encrypted_file.kdf, "HKDF-SHA256"); @@ -56,7 +56,7 @@ mod tests { assert_eq!(encrypted_file.did, did.0); // Decrypt - let (decrypted_seed, decrypted_public_key, _, _) = encryptor.decrypt(&encrypted_file, password).unwrap(); + let (decrypted_seed, decrypted_public_key, _, _, _) = encryptor.decrypt(&encrypted_file, password).unwrap(); // Verify decryption assert_eq!(decrypted_seed.as_bytes(), seed.as_bytes()); @@ -72,7 +72,7 @@ mod tests { let did = Did::new(&public_key); // Encrypt with one password - let encrypted_file = encryptor.encrypt(&seed, "correct-password", &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[]).unwrap(); + let encrypted_file = encryptor.encrypt(&seed, "correct-password", &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[], &None).unwrap(); // Try to decrypt with wrong password let result = encryptor.decrypt(&encrypted_file, "wrong-password"); diff --git a/libs/sharenet-passport/src/infrastructure/crypto/wasm.rs b/libs/sharenet-passport/src/infrastructure/crypto/wasm.rs index 3367ba8..980fa54 100644 --- a/libs/sharenet-passport/src/infrastructure/crypto/wasm.rs +++ b/libs/sharenet-passport/src/infrastructure/crypto/wasm.rs @@ -91,6 +91,7 @@ impl FileEncryptor for XChaCha20FileEncryptor { did: &Did, univ_id: &str, user_profiles: &[UserProfile], + date_of_birth: &Option, ) -> Result { // Generate salt and nonce using WASM-compatible RNG let mut salt = [0u8; SALT_LENGTH]; @@ -120,6 +121,13 @@ impl FileEncryptor for XChaCha20FileEncryptor { .encrypt(&nonce, &*user_profiles_bytes) .map_err(|e| DomainError::CryptographicError(format!("User profiles encryption failed: {}", e)))?; + // Serialize and encrypt date of birth + let date_of_birth_bytes = serde_cbor::to_vec(&date_of_birth) + .map_err(|e| DomainError::CryptographicError(format!("Failed to serialize date of birth: {}", e)))?; + let enc_date_of_birth = cipher + .encrypt(&nonce, &*date_of_birth_bytes) + .map_err(|e| DomainError::CryptographicError(format!("Date of birth encryption failed: {}", e)))?; + // Get current timestamp using WASM-compatible time let created_at = time::now_seconds()?; @@ -135,6 +143,7 @@ impl FileEncryptor for XChaCha20FileEncryptor { created_at, version: "1.0.0".to_string(), enc_user_profiles, + enc_date_of_birth, }) } @@ -142,7 +151,7 @@ impl FileEncryptor for XChaCha20FileEncryptor { &self, file: &PassportFile, password: &str, - ) -> Result<(Seed, PublicKey, PrivateKey, Vec), Self::Error> { + ) -> Result<(Seed, PublicKey, PrivateKey, Vec, Option), Self::Error> { // Validate file format validate_file_format(&file.kdf, &file.cipher)?; @@ -179,7 +188,14 @@ impl FileEncryptor for XChaCha20FileEncryptor { let user_profiles: Vec = serde_cbor::from_slice(&user_profiles_bytes) .map_err(|e| DomainError::CryptographicError(format!("Failed to deserialize user profiles: {}", e)))?; + // Decrypt date of birth + let date_of_birth_bytes = cipher + .decrypt(&nonce, &*file.enc_date_of_birth) + .map_err(|e| DomainError::CryptographicError(format!("Date of birth decryption failed: {}", e)))?; + let date_of_birth: Option = serde_cbor::from_slice(&date_of_birth_bytes) + .map_err(|e| DomainError::CryptographicError(format!("Failed to deserialize date of birth: {}", e)))?; + // Note: univ_id is stored in the PassportFile and will be used when creating the Passport - Ok((seed, public_key, private_key, user_profiles)) + Ok((seed, public_key, private_key, user_profiles, date_of_birth)) } } \ No newline at end of file diff --git a/libs/sharenet-passport/src/infrastructure/storage/native_test.rs b/libs/sharenet-passport/src/infrastructure/storage/native_test.rs index 4b4168a..45aaf72 100644 --- a/libs/sharenet-passport/src/infrastructure/storage/native_test.rs +++ b/libs/sharenet-passport/src/infrastructure/storage/native_test.rs @@ -25,6 +25,7 @@ mod tests { created_at: 1234567890, version: "1.0.0".to_string(), enc_user_profiles: vec![], + enc_date_of_birth: vec![], }; // Save the file diff --git a/libs/sharenet-passport/src/wasm.rs b/libs/sharenet-passport/src/wasm.rs index f51d8c5..350694c 100644 --- a/libs/sharenet-passport/src/wasm.rs +++ b/libs/sharenet-passport/src/wasm.rs @@ -114,7 +114,7 @@ pub fn import_from_encrypted_data( // Decrypt the passport file using the password let encryptor = XChaCha20FileEncryptor; - let (seed, public_key, private_key, user_profiles) = encryptor.decrypt( + let (seed, public_key, private_key, user_profiles, date_of_birth) = encryptor.decrypt( &passport_file, &password, ).map_err(|e| JsValue::from_str(&format!("Failed to decrypt passport: {}", e)))?; @@ -128,6 +128,7 @@ pub fn import_from_encrypted_data( did, univ_id: passport_file.univ_id, user_profiles, + date_of_birth, }; let result = serde_wasm_bindgen::to_value(&passport) @@ -155,6 +156,7 @@ pub fn export_to_encrypted_data( &passport.did, &passport.univ_id, &passport.user_profiles, + &passport.date_of_birth, ).map_err(|e| JsValue::from_str(&format!("Failed to encrypt passport: {}", e)))?; // Serialize to bytes for browser download diff --git a/sharenet-passport-cli/src/cli/interface.rs b/sharenet-passport-cli/src/cli/interface.rs index 0fd3753..eb37640 100644 --- a/sharenet-passport-cli/src/cli/interface.rs +++ b/sharenet-passport-cli/src/cli/interface.rs @@ -285,6 +285,7 @@ impl CliInterface { language, notifications_enabled: notifications, auto_sync, + show_date_of_birth: false, }; let create_use_case = CreateUserProfileUseCase::new( @@ -357,6 +358,7 @@ impl CliInterface { language: language.or_else(|| existing_profile.preferences.language.clone()), notifications_enabled: notifications.unwrap_or(existing_profile.preferences.notifications_enabled), auto_sync: auto_sync.unwrap_or(existing_profile.preferences.auto_sync), + show_date_of_birth: existing_profile.preferences.show_date_of_birth, }; // Clone values before using them in multiple places