Add lots more tests
Some checks are pending
Sharenet Passport CI / test-native (push) Waiting to run
Sharenet Passport CI / test-wasm-headless (push) Waiting to run
Sharenet Passport CI / test-wasm-webdriver (push) Waiting to run
Sharenet Passport CI / build-wasm (push) Waiting to run
Sharenet Passport CI / lint (push) Waiting to run

This commit is contained in:
Continuist 2025-10-31 00:17:16 -04:00
parent 8d0d203182
commit bd4c3ac3ab
15 changed files with 3664 additions and 29 deletions

View file

@ -81,6 +81,7 @@ where
&passport.univ_id, &passport.univ_id,
&passport.user_profiles, &passport.user_profiles,
&passport.date_of_birth, &passport.date_of_birth,
&passport.default_user_profile_id,
) )
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?; .map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
@ -171,6 +172,7 @@ where
&passport.univ_id, &passport.univ_id,
&passport.user_profiles, &passport.user_profiles,
&passport.date_of_birth, &passport.date_of_birth,
&passport.default_user_profile_id,
) )
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?; .map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
@ -219,7 +221,7 @@ where
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to load file: {}", e.into())))?; .map_err(|e| ApplicationError::UseCaseError(format!("Failed to load file: {}", e.into())))?;
// Decrypt file // Decrypt file
let (seed, public_key, private_key, user_profiles, date_of_birth) = self let (seed, public_key, private_key, user_profiles, date_of_birth, default_user_profile_id) = self
.file_encryptor .file_encryptor
.decrypt(&passport_file, password) .decrypt(&passport_file, password)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to decrypt file: {}", e.into())))?; .map_err(|e| ApplicationError::UseCaseError(format!("Failed to decrypt file: {}", e.into())))?;
@ -234,6 +236,7 @@ where
); );
passport.user_profiles = user_profiles; passport.user_profiles = user_profiles;
passport.date_of_birth = date_of_birth; passport.date_of_birth = date_of_birth;
passport.default_user_profile_id = default_user_profile_id;
// Re-encrypt and save if output path provided // Re-encrypt and save if output path provided
if let Some(output_path) = output_path { if let Some(output_path) = output_path {
@ -247,6 +250,7 @@ where
&passport.univ_id, &passport.univ_id,
&passport.user_profiles, &passport.user_profiles,
&passport.date_of_birth, &passport.date_of_birth,
&passport.default_user_profile_id,
) )
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to re-encrypt file: {}", e.into())))?; .map_err(|e| ApplicationError::UseCaseError(format!("Failed to re-encrypt file: {}", e.into())))?;
@ -296,6 +300,7 @@ where
&passport.univ_id, &passport.univ_id,
&passport.user_profiles, &passport.user_profiles,
&passport.date_of_birth, &passport.date_of_birth,
&passport.default_user_profile_id,
) )
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?; .map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
@ -383,6 +388,7 @@ where
&passport.univ_id, &passport.univ_id,
&passport.user_profiles, &passport.user_profiles,
&passport.date_of_birth, &passport.date_of_birth,
&passport.default_user_profile_id,
) )
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?; .map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
@ -419,6 +425,7 @@ where
&self, &self,
passport: &mut Passport, passport: &mut Passport,
id: Option<&str>, id: Option<&str>,
hub_did: Option<String>,
identity: UserIdentity, identity: UserIdentity,
preferences: UserPreferences, preferences: UserPreferences,
password: &str, password: &str,
@ -434,10 +441,10 @@ where
let now = time::now_seconds() let now = time::now_seconds()
.map_err(|e| ApplicationError::UseCaseError(format!("Time error: {}", e)))?; .map_err(|e| ApplicationError::UseCaseError(format!("Time error: {}", e)))?;
// Use existing hub_did (cannot change hub_did via update) // Use provided hub_did or keep existing
let profile = UserProfile { let profile = UserProfile {
id: existing_profile.id.clone(), id: existing_profile.id.clone(),
hub_did: existing_profile.hub_did.clone(), hub_did: hub_did.or_else(|| existing_profile.hub_did.clone()),
identity, identity,
preferences, preferences,
created_at: existing_profile.created_at, created_at: existing_profile.created_at,
@ -458,6 +465,7 @@ where
&passport.univ_id, &passport.univ_id,
&passport.user_profiles, &passport.user_profiles,
&passport.date_of_birth, &passport.date_of_birth,
&passport.default_user_profile_id,
) )
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?; .map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
@ -514,6 +522,7 @@ where
&passport.univ_id, &passport.univ_id,
&passport.user_profiles, &passport.user_profiles,
&passport.date_of_birth, &passport.date_of_birth,
&passport.default_user_profile_id,
) )
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?; .map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;

View file

@ -233,6 +233,7 @@ mod tests {
let result = update_profile_use_case.execute( let result = update_profile_use_case.execute(
&mut passport, &mut passport,
Some(&profile_id), Some(&profile_id),
Some("h:example".to_string()),
updated_identity, updated_identity,
updated_preferences, updated_preferences,
"test-password", "test-password",
@ -293,6 +294,7 @@ mod tests {
let result = update_profile_use_case.execute( let result = update_profile_use_case.execute(
&mut passport, &mut passport,
Some("non-existent-id"), Some("non-existent-id"),
Some("h:example".to_string()),
identity, identity,
preferences, preferences,
"test-password", "test-password",

View file

@ -81,6 +81,7 @@ pub struct Passport {
pub univ_id: String, pub univ_id: String,
pub user_profiles: Vec<UserProfile>, pub user_profiles: Vec<UserProfile>,
pub date_of_birth: Option<DateOfBirth>, pub date_of_birth: Option<DateOfBirth>,
pub default_user_profile_id: Option<String>, // UUIDv7 of the default user profile
} }
impl Passport { impl Passport {
@ -119,8 +120,9 @@ impl Passport {
private_key, private_key,
did, did,
univ_id, univ_id,
user_profiles: vec![default_profile], user_profiles: vec![default_profile.clone()],
date_of_birth: None, date_of_birth: None,
default_user_profile_id: Some(default_profile.id.clone()),
} }
} }
@ -141,8 +143,13 @@ impl Passport {
} }
pub fn default_user_profile(&self) -> Option<&UserProfile> { pub fn default_user_profile(&self) -> Option<&UserProfile> {
if let Some(default_id) = &self.default_user_profile_id {
self.user_profile_by_id(default_id)
} else {
// Fallback to implicit detection for backward compatibility
self.user_profiles.iter().find(|p| p.is_default()) self.user_profiles.iter().find(|p| p.is_default())
} }
}
pub fn user_profile_for_hub(&self, hub_did: &str) -> Option<&UserProfile> { pub fn user_profile_for_hub(&self, hub_did: &str) -> Option<&UserProfile> {
self.user_profiles.iter().find(|p| p.hub_did.as_deref() == Some(hub_did)) self.user_profiles.iter().find(|p| p.hub_did.as_deref() == Some(hub_did))
@ -157,10 +164,13 @@ impl Passport {
} }
pub fn add_user_profile(&mut self, profile: UserProfile) -> Result<(), String> { pub fn add_user_profile(&mut self, profile: UserProfile) -> Result<(), String> {
// Ensure only one default profile // If this is a default profile (no hub_did), set it as the default
if profile.is_default() && self.default_user_profile().is_some() { if profile.hub_did.is_none() {
if self.default_user_profile_id.is_some() {
return Err("Default user profile already exists".to_string()); return Err("Default user profile already exists".to_string());
} }
self.default_user_profile_id = Some(profile.id.clone());
}
// Ensure hub_did is unique // Ensure hub_did is unique
if let Some(hub_did) = &profile.hub_did { if let Some(hub_did) = &profile.hub_did {
@ -225,7 +235,7 @@ impl Passport {
match index { match index {
Some(idx) => { Some(idx) => {
// Check if this is the default profile // Check if this is the default profile
if self.user_profiles[idx].is_default() { if self.default_user_profile_id.as_deref() == Some(profile_id) {
return Err("Cannot delete default user profile".to_string()); return Err("Cannot delete default user profile".to_string());
} }
self.user_profiles.remove(idx); self.user_profiles.remove(idx);
@ -234,6 +244,23 @@ impl Passport {
None => Err("User profile not found".to_string()), None => Err("User profile not found".to_string()),
} }
} }
pub fn set_default_user_profile(&mut self, profile_id: &str) -> Result<(), String> {
// Verify the profile exists
if self.user_profile_by_id(profile_id).is_none() {
return Err("User profile not found".to_string());
}
// Verify the profile is a default profile (no hub_did)
if let Some(profile) = self.user_profile_by_id(profile_id) {
if profile.hub_did.is_some() {
return Err("Cannot set hub-specific profile as default".to_string());
}
}
self.default_user_profile_id = Some(profile_id.to_string());
Ok(())
}
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -310,4 +337,5 @@ pub struct PassportFile {
pub version: String, pub version: String,
pub enc_user_profiles: Vec<u8>, // Encrypted CBOR of Vec<UserProfile> pub enc_user_profiles: Vec<u8>, // Encrypted CBOR of Vec<UserProfile>
pub enc_date_of_birth: Vec<u8>, // Encrypted CBOR of Option<DateOfBirth> pub enc_date_of_birth: Vec<u8>, // Encrypted CBOR of Option<DateOfBirth>
pub enc_default_user_profile_id: Vec<u8>, // Encrypted CBOR of Option<String>
} }

View file

@ -27,13 +27,14 @@ pub trait FileEncryptor {
univ_id: &str, univ_id: &str,
user_profiles: &[UserProfile], user_profiles: &[UserProfile],
date_of_birth: &Option<DateOfBirth>, date_of_birth: &Option<DateOfBirth>,
default_user_profile_id: &Option<String>,
) -> Result<PassportFile, Self::Error>; ) -> Result<PassportFile, Self::Error>;
fn decrypt( fn decrypt(
&self, &self,
file: &PassportFile, file: &PassportFile,
password: &str, password: &str,
) -> Result<(Seed, PublicKey, PrivateKey, Vec<UserProfile>, Option<DateOfBirth>), Self::Error>; ) -> Result<(Seed, PublicKey, PrivateKey, Vec<UserProfile>, Option<DateOfBirth>, Option<String>), Self::Error>;
} }
pub trait FileStorage { pub trait FileStorage {

View file

@ -91,6 +91,7 @@ impl FileEncryptor for XChaCha20FileEncryptor {
univ_id: &str, univ_id: &str,
user_profiles: &[UserProfile], user_profiles: &[UserProfile],
date_of_birth: &Option<DateOfBirth>, date_of_birth: &Option<DateOfBirth>,
default_user_profile_id: &Option<String>,
) -> Result<PassportFile, Self::Error> { ) -> Result<PassportFile, Self::Error> {
// Generate salt and nonce // Generate salt and nonce
let mut salt = [0u8; SALT_LENGTH]; let mut salt = [0u8; SALT_LENGTH];
@ -126,6 +127,13 @@ impl FileEncryptor for XChaCha20FileEncryptor {
.encrypt(&nonce, &*date_of_birth_bytes) .encrypt(&nonce, &*date_of_birth_bytes)
.map_err(|e| DomainError::CryptographicError(format!("Date of birth encryption failed: {}", e)))?; .map_err(|e| DomainError::CryptographicError(format!("Date of birth encryption failed: {}", e)))?;
// Serialize and encrypt default user profile ID
let default_user_profile_id_bytes = serde_cbor::to_vec(&default_user_profile_id)
.map_err(|e| DomainError::CryptographicError(format!("Failed to serialize default user profile ID: {}", e)))?;
let enc_default_user_profile_id = cipher
.encrypt(&nonce, &*default_user_profile_id_bytes)
.map_err(|e| DomainError::CryptographicError(format!("Default user profile ID encryption failed: {}", e)))?;
// Get current timestamp // Get current timestamp
let created_at = SystemTime::now() let created_at = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
@ -145,6 +153,7 @@ impl FileEncryptor for XChaCha20FileEncryptor {
version: "1.0.0".to_string(), version: "1.0.0".to_string(),
enc_user_profiles, enc_user_profiles,
enc_date_of_birth, enc_date_of_birth,
enc_default_user_profile_id,
}) })
} }
@ -152,7 +161,7 @@ impl FileEncryptor for XChaCha20FileEncryptor {
&self, &self,
file: &PassportFile, file: &PassportFile,
password: &str, password: &str,
) -> Result<(Seed, PublicKey, PrivateKey, Vec<UserProfile>, Option<DateOfBirth>), Self::Error> { ) -> Result<(Seed, PublicKey, PrivateKey, Vec<UserProfile>, Option<DateOfBirth>, Option<String>), Self::Error> {
// Validate file format // Validate file format
validate_file_format(&file.kdf, &file.cipher)?; validate_file_format(&file.kdf, &file.cipher)?;
@ -196,7 +205,14 @@ impl FileEncryptor for XChaCha20FileEncryptor {
let date_of_birth: Option<DateOfBirth> = serde_cbor::from_slice(&date_of_birth_bytes) let date_of_birth: Option<DateOfBirth> = serde_cbor::from_slice(&date_of_birth_bytes)
.map_err(|e| DomainError::CryptographicError(format!("Failed to deserialize date of birth: {}", e)))?; .map_err(|e| DomainError::CryptographicError(format!("Failed to deserialize date of birth: {}", e)))?;
// Decrypt default user profile ID
let default_user_profile_id_bytes = cipher
.decrypt(&nonce, &*file.enc_default_user_profile_id)
.map_err(|e| DomainError::CryptographicError(format!("Default user profile ID decryption failed: {}", e)))?;
let default_user_profile_id: Option<String> = serde_cbor::from_slice(&default_user_profile_id_bytes)
.map_err(|e| DomainError::CryptographicError(format!("Failed to deserialize default user profile ID: {}", e)))?;
// Note: univ_id is stored in the PassportFile and will be used when creating the Passport // Note: univ_id is stored in the PassportFile and will be used when creating the Passport
Ok((seed, public_key, private_key, user_profiles, date_of_birth)) Ok((seed, public_key, private_key, user_profiles, date_of_birth, default_user_profile_id))
} }
} }

View file

@ -45,7 +45,7 @@ mod tests {
let password = "test-password"; let password = "test-password";
// Encrypt // Encrypt
let encrypted_file = encryptor.encrypt(&seed, password, &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[], &None).unwrap(); let encrypted_file = encryptor.encrypt(&seed, password, &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[], &None, &None).unwrap();
// Verify file structure // Verify file structure
assert_eq!(encrypted_file.kdf, "HKDF-SHA256"); assert_eq!(encrypted_file.kdf, "HKDF-SHA256");
@ -56,7 +56,7 @@ mod tests {
assert_eq!(encrypted_file.did, did.0); assert_eq!(encrypted_file.did, did.0);
// Decrypt // 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 // Verify decryption
assert_eq!(decrypted_seed.as_bytes(), seed.as_bytes()); assert_eq!(decrypted_seed.as_bytes(), seed.as_bytes());
@ -72,7 +72,7 @@ mod tests {
let did = Did::new(&public_key); let did = Did::new(&public_key);
// Encrypt with one password // Encrypt with one password
let encrypted_file = encryptor.encrypt(&seed, "correct-password", &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[], &None).unwrap(); let encrypted_file = encryptor.encrypt(&seed, "correct-password", &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[], &None, &None).unwrap();
// Try to decrypt with wrong password // Try to decrypt with wrong password
let result = encryptor.decrypt(&encrypted_file, "wrong-password"); let result = encryptor.decrypt(&encrypted_file, "wrong-password");

View file

@ -45,7 +45,7 @@ mod tests {
let password = "test-password"; let password = "test-password";
// Encrypt // 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, &None).unwrap();
// Verify file structure // Verify file structure
assert_eq!(encrypted_file.kdf, "HKDF-SHA256"); assert_eq!(encrypted_file.kdf, "HKDF-SHA256");
@ -56,7 +56,7 @@ mod tests {
assert_eq!(encrypted_file.did, did.0); assert_eq!(encrypted_file.did, did.0);
// Decrypt // 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 // Verify decryption
assert_eq!(decrypted_seed.as_bytes(), seed.as_bytes()); assert_eq!(decrypted_seed.as_bytes(), seed.as_bytes());
@ -72,7 +72,7 @@ mod tests {
let did = Did::new(&public_key); let did = Did::new(&public_key);
// Encrypt with one password // 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, &None).unwrap();
// Try to decrypt with wrong password // Try to decrypt with wrong password
let result = encryptor.decrypt(&encrypted_file, "wrong-password"); let result = encryptor.decrypt(&encrypted_file, "wrong-password");

View file

@ -26,6 +26,7 @@ mod tests {
version: "1.0.0".to_string(), version: "1.0.0".to_string(),
enc_user_profiles: vec![], enc_user_profiles: vec![],
enc_date_of_birth: vec![], enc_date_of_birth: vec![],
enc_default_user_profile_id: vec![],
}; };
// Save the file // Save the file

View file

@ -24,6 +24,8 @@ mod tests {
created_at: 1234567890, created_at: 1234567890,
version: "1.0.0".to_string(), version: "1.0.0".to_string(),
enc_user_profiles: vec![], enc_user_profiles: vec![],
enc_date_of_birth: vec![],
enc_default_user_profile_id: vec![],
}; };
// Save the file // Save the file

View file

@ -114,7 +114,7 @@ pub fn import_from_encrypted_data(
// Decrypt the passport file using the password // Decrypt the passport file using the password
let encryptor = XChaCha20FileEncryptor; let encryptor = XChaCha20FileEncryptor;
let (seed, public_key, private_key, user_profiles, date_of_birth) = encryptor.decrypt( let (seed, public_key, private_key, user_profiles, date_of_birth, default_user_profile_id) = encryptor.decrypt(
&passport_file, &passport_file,
&password, &password,
).map_err(|e| JsValue::from_str(&format!("Failed to decrypt passport: {}", e)))?; ).map_err(|e| JsValue::from_str(&format!("Failed to decrypt passport: {}", e)))?;
@ -129,6 +129,7 @@ pub fn import_from_encrypted_data(
univ_id: passport_file.univ_id, univ_id: passport_file.univ_id,
user_profiles, user_profiles,
date_of_birth, date_of_birth,
default_user_profile_id,
}; };
let result = serde_wasm_bindgen::to_value(&passport) let result = serde_wasm_bindgen::to_value(&passport)
@ -157,6 +158,7 @@ pub fn export_to_encrypted_data(
&passport.univ_id, &passport.univ_id,
&passport.user_profiles, &passport.user_profiles,
&passport.date_of_birth, &passport.date_of_birth,
&passport.default_user_profile_id,
).map_err(|e| JsValue::from_str(&format!("Failed to encrypt passport: {}", e)))?; ).map_err(|e| JsValue::from_str(&format!("Failed to encrypt passport: {}", e)))?;
// Serialize to bytes for browser download // Serialize to bytes for browser download

View file

@ -65,6 +65,26 @@ pub enum Commands {
file: String, file: String,
}, },
/// Display complete decrypted Passport data
Show {
/// .spf file path
file: String,
},
/// Edit global Passport fields
Edit {
/// .spf file path
file: String,
/// Date of birth (format: MM-DD-YYYY)
#[arg(long, conflicts_with = "remove_date_of_birth")]
date_of_birth: Option<String>,
/// Remove date of birth
#[arg(long, conflicts_with = "date_of_birth")]
remove_date_of_birth: bool,
},
/// Sign a message (for testing) /// Sign a message (for testing)
Sign { Sign {
/// .spf file path /// .spf file path
@ -95,7 +115,7 @@ pub enum ProfileCommands {
file: String, file: String,
/// Hub DID (optional, omit for default profile) /// Hub DID (optional, omit for default profile)
#[arg(short, long)] #[arg(long)]
hub_did: Option<String>, hub_did: Option<String>,
/// Handle /// Handle
@ -149,8 +169,12 @@ pub enum ProfileCommands {
file: String, file: String,
/// Profile ID (required, use 'list' command to see available IDs) /// Profile ID (required, use 'list' command to see available IDs)
#[arg(short, long)] #[arg(short, long, conflicts_with = "default")]
id: String, id: Option<String>,
/// Update the default user profile
#[arg(long, conflicts_with = "id")]
default: bool,
/// Hub DID (optional, can be updated) /// Hub DID (optional, can be updated)
#[arg(long)] #[arg(long)]
@ -199,6 +223,10 @@ pub enum ProfileCommands {
/// Enable auto-sync /// Enable auto-sync
#[arg(long)] #[arg(long)]
auto_sync: Option<bool>, auto_sync: Option<bool>,
/// Show date of birth
#[arg(long)]
show_date_of_birth: Option<bool>,
}, },
/// Delete a user profile /// Delete a user profile

View file

@ -29,6 +29,13 @@ impl CliInterface {
} }
pub fn handle_create(&self, universe: &str, output: &str) -> Result<(), ApplicationError> { pub fn handle_create(&self, universe: &str, output: &str) -> Result<(), ApplicationError> {
// Validate universe ID format
if !universe.starts_with("u:") {
return Err(ApplicationError::UseCaseError(
"Invalid universe ID format. Must start with 'u:'".to_string()
));
}
let password = prompt_password("Enter password for new passport: ") let password = prompt_password("Enter password for new passport: ")
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?; .map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
let confirm_password = prompt_password("Confirm password: ") let confirm_password = prompt_password("Confirm password: ")
@ -64,7 +71,22 @@ impl CliInterface {
for i in 1..=24 { for i in 1..=24 {
let word = prompt_password(&format!("Word {}: ", i)) let word = prompt_password(&format!("Word {}: ", i))
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read recovery word: {}", e)))?; .map_err(|e| ApplicationError::UseCaseError(format!("Failed to read recovery word: {}", e)))?;
recovery_words.push(word);
// Validate recovery word is not empty
if word.trim().is_empty() {
return Err(ApplicationError::UseCaseError(
format!("Recovery word {} cannot be empty", i)
));
}
recovery_words.push(word.trim().to_lowercase());
}
// Validate that all words are non-empty
if recovery_words.iter().any(|word| word.is_empty()) {
return Err(ApplicationError::UseCaseError(
"Recovery phrase contains empty words".to_string()
));
} }
let password = prompt_password("Enter new password for passport file: ") let password = prompt_password("Enter new password for passport file: ")
@ -165,6 +187,187 @@ impl CliInterface {
Ok(()) Ok(())
} }
pub fn handle_show(&self, file: &str) -> Result<(), ApplicationError> {
let password = prompt_password("Enter password for passport file: ")
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
let import_use_case = ImportFromFileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let passport = import_use_case.execute(file, &password, None)?;
println!("🔓 Complete Decrypted Passport Data:");
println!(" File: {}", file);
println!(" Universe ID: {}", passport.univ_id());
println!(" DID: {}", passport.did().as_str());
println!(" Public Key: {}", hex::encode(&passport.public_key.0));
println!(" Private Key: {} (⚠️ SENSITIVE - DO NOT SHARE)", hex::encode(&passport.private_key.0));
println!(" Seed: {} (⚠️ SENSITIVE - DO NOT SHARE)", hex::encode(passport.seed.as_bytes()));
if let Some(date_of_birth) = &passport.date_of_birth {
println!(" Date of Birth: {}-{}-{}", date_of_birth.month, date_of_birth.day, date_of_birth.year);
} else {
println!(" Date of Birth: Not set");
}
if let Some(default_profile_id) = &passport.default_user_profile_id {
println!(" Default User Profile ID: {}", default_profile_id);
} else {
println!(" Default User Profile ID: Not set");
}
println!("\n👤 User Profiles ({} total):", passport.user_profiles().len());
for (i, profile) in passport.user_profiles().iter().enumerate() {
println!("\n {}. Profile ID: {}", i + 1, profile.id);
println!(" Profile Type: {}", if profile.is_default() { "Default" } else { "Hub-specific" });
if let Some(hub_did) = &profile.hub_did {
println!(" Hub DID: {}", hub_did);
}
println!(" Created: {}", profile.created_at);
println!(" Updated: {}", profile.updated_at);
println!(" Identity:");
if let Some(handle) = &profile.identity.handle {
println!(" Handle: {}", handle);
}
if let Some(name) = &profile.identity.display_name {
println!(" Display Name: {}", name);
}
if let Some(first_name) = &profile.identity.first_name {
println!(" First Name: {}", first_name);
}
if let Some(last_name) = &profile.identity.last_name {
println!(" Last Name: {}", last_name);
}
if let Some(email) = &profile.identity.email {
println!(" Email: {}", email);
}
if let Some(avatar) = &profile.identity.avatar_url {
println!(" Avatar URL: {}", avatar);
}
if let Some(bio) = &profile.identity.bio {
println!(" Bio: {}", bio);
}
println!(" Preferences:");
if let Some(theme) = &profile.preferences.theme {
println!(" Theme: {}", theme);
}
if let Some(language) = &profile.preferences.language {
println!(" Language: {}", language);
}
println!(" Notifications: {}", if profile.preferences.notifications_enabled { "Enabled" } else { "Disabled" });
println!(" Auto-sync: {}", if profile.preferences.auto_sync { "Enabled" } else { "Disabled" });
println!(" Show Date of Birth: {}", if profile.preferences.show_date_of_birth { "Yes" } else { "No" });
}
println!("\n⚠️ SECURITY WARNING:");
println!(" - Private key and seed are sensitive cryptographic material");
println!(" - Never share these values with anyone");
println!(" - Keep this information secure and confidential");
Ok(())
}
pub fn handle_edit(
&self,
file: &str,
date_of_birth: Option<String>,
remove_date_of_birth: bool,
) -> Result<(), ApplicationError> {
let password = prompt_password("Enter password for passport file: ")
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
let import_use_case = ImportFromFileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let mut passport = import_use_case.execute(file, &password, None)?;
let mut changes_made = false;
// Handle date of birth changes
if remove_date_of_birth {
passport.date_of_birth = None;
changes_made = true;
println!("🗑️ Date of birth removed");
} else if let Some(dob_str) = date_of_birth {
// Parse date of birth string (format: MM-DD-YYYY)
let parts: Vec<&str> = dob_str.split('-').collect();
if parts.len() != 3 {
return Err(ApplicationError::UseCaseError(
"Invalid date format. Use MM-DD-YYYY".to_string()
));
}
let month = parts[0].parse::<u8>()
.map_err(|_| ApplicationError::UseCaseError("Invalid month".to_string()))?;
let day = parts[1].parse::<u8>()
.map_err(|_| ApplicationError::UseCaseError("Invalid day".to_string()))?;
let year = parts[2].parse::<u16>()
.map_err(|_| ApplicationError::UseCaseError("Invalid year".to_string()))?;
// Basic validation
if month < 1 || month > 12 {
return Err(ApplicationError::UseCaseError("Month must be between 1 and 12".to_string()));
}
if day < 1 || day > 31 {
return Err(ApplicationError::UseCaseError("Day must be between 1 and 31".to_string()));
}
if year < 1900 || year > 2100 {
return Err(ApplicationError::UseCaseError("Year must be between 1900 and 2100".to_string()));
}
// Comprehensive date validation
let max_days = match month {
2 => {
// February - check for leap year
let is_leap_year = (year % 4 == 0) && (year % 100 != 0 || year % 400 == 0);
if is_leap_year { 29 } else { 28 }
}
4 | 6 | 9 | 11 => 30, // April, June, September, November
_ => 31, // January, March, May, July, August, October, December
};
if day > max_days {
return Err(ApplicationError::UseCaseError(
format!("Invalid day {} for month {}. Maximum days for this month is {}", day, month, max_days)
));
}
let new_dob = sharenet_passport::domain::entities::DateOfBirth {
month,
day,
year,
};
passport.date_of_birth = Some(new_dob);
changes_made = true;
println!("📅 Date of birth set to: {}-{}-{}", month, day, year);
}
if !changes_made {
println!(" No changes specified. Use --date-of-birth or --remove-date-of-birth");
return Ok(());
}
// Save the updated passport
let export_use_case = ExportPassportUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
export_use_case.execute(&passport, &password, file)?;
println!("✅ Passport updated successfully!");
println!("📄 Saved to: {}", file);
Ok(())
}
pub fn handle_sign(&self, file: &str, message: &str) -> Result<(), ApplicationError> { pub fn handle_sign(&self, file: &str, message: &str) -> Result<(), ApplicationError> {
let password = prompt_password("Enter password for passport file: ") let password = prompt_password("Enter password for passport file: ")
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?; .map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
@ -239,6 +442,7 @@ impl CliInterface {
} }
println!(" Notifications: {}", if profile.preferences.notifications_enabled { "Enabled" } else { "Disabled" }); println!(" Notifications: {}", if profile.preferences.notifications_enabled { "Enabled" } else { "Disabled" });
println!(" Auto-sync: {}", if profile.preferences.auto_sync { "Enabled" } else { "Disabled" }); println!(" Auto-sync: {}", if profile.preferences.auto_sync { "Enabled" } else { "Disabled" });
println!(" Show Date of Birth: {}", if profile.preferences.show_date_of_birth { "Yes" } else { "No" });
} }
Ok(()) Ok(())
@ -315,7 +519,8 @@ impl CliInterface {
pub fn handle_profile_update( pub fn handle_profile_update(
&self, &self,
file: &str, file: &str,
id: &str, id: Option<&str>,
default: bool,
hub_did: Option<String>, hub_did: Option<String>,
handle: Option<String>, handle: Option<String>,
display_name: Option<String>, display_name: Option<String>,
@ -328,6 +533,7 @@ impl CliInterface {
language: Option<String>, language: Option<String>,
notifications: Option<bool>, notifications: Option<bool>,
auto_sync: Option<bool>, auto_sync: Option<bool>,
show_date_of_birth: Option<bool>,
) -> Result<(), ApplicationError> { ) -> Result<(), ApplicationError> {
let password = prompt_password("Enter password for passport file: ") let password = prompt_password("Enter password for passport file: ")
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?; .map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
@ -339,8 +545,23 @@ impl CliInterface {
let mut passport = import_use_case.execute(file, &password, None)?; let mut passport = import_use_case.execute(file, &password, None)?;
// Determine which profile to update and get profile ID
let profile_id = if default {
// Update the default profile
let default_profile = passport.default_user_profile()
.ok_or_else(|| ApplicationError::UseCaseError("Default user profile not found".to_string()))?;
Some(default_profile.id.clone())
} else if let Some(id) = id {
// Update specific profile by ID
Some(id.to_string())
} else {
return Err(ApplicationError::UseCaseError(
"Either --id or --default must be specified".to_string()
));
};
// Get existing profile by ID // Get existing profile by ID
let existing_profile = passport.user_profile_by_id(id) let existing_profile = passport.user_profile_by_id(&profile_id.clone().unwrap())
.ok_or_else(|| ApplicationError::UseCaseError("User profile not found".to_string()))?; .ok_or_else(|| ApplicationError::UseCaseError("User profile not found".to_string()))?;
let identity = UserIdentity { let identity = UserIdentity {
@ -358,13 +579,14 @@ impl CliInterface {
language: language.or_else(|| existing_profile.preferences.language.clone()), language: language.or_else(|| existing_profile.preferences.language.clone()),
notifications_enabled: notifications.unwrap_or(existing_profile.preferences.notifications_enabled), notifications_enabled: notifications.unwrap_or(existing_profile.preferences.notifications_enabled),
auto_sync: auto_sync.unwrap_or(existing_profile.preferences.auto_sync), auto_sync: auto_sync.unwrap_or(existing_profile.preferences.auto_sync),
show_date_of_birth: existing_profile.preferences.show_date_of_birth, show_date_of_birth: show_date_of_birth.unwrap_or(existing_profile.preferences.show_date_of_birth),
}; };
// Clone values before using them in multiple places // Clone values before using them in multiple places
let identity_clone = identity.clone(); let identity_clone = identity.clone();
let preferences_clone = preferences.clone(); let preferences_clone = preferences.clone();
let hub_did_clone = hub_did.clone(); let hub_did_clone = hub_did.clone();
let hub_did_for_use_case = hub_did.clone();
// Create updated profile with new hub_did if provided // Create updated profile with new hub_did if provided
let now = std::time::SystemTime::now() let now = std::time::SystemTime::now()
@ -389,7 +611,8 @@ impl CliInterface {
update_use_case.execute( update_use_case.execute(
&mut passport, &mut passport,
Some(id), profile_id.as_deref(),
hub_did_for_use_case,
identity_clone, identity_clone,
preferences_clone, preferences_clone,
&password, &password,

View file

@ -1,2 +1,5 @@
pub mod commands; pub mod commands;
pub mod interface; pub mod interface;
#[cfg(test)]
pub mod tests;

File diff suppressed because it is too large Load diff

View file

@ -28,6 +28,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Commands::Info { file } => { Commands::Info { file } => {
interface.handle_info(&file)?; interface.handle_info(&file)?;
} }
Commands::Show { file } => {
interface.handle_show(&file)?;
}
Commands::Edit { file, date_of_birth, remove_date_of_birth } => {
interface.handle_edit(&file, date_of_birth, remove_date_of_birth)?;
}
Commands::Sign { file, message } => { Commands::Sign { file, message } => {
interface.handle_sign(&file, &message)?; interface.handle_sign(&file, &message)?;
} }
@ -70,6 +76,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
crate::cli::commands::ProfileCommands::Update { crate::cli::commands::ProfileCommands::Update {
file, file,
id, id,
default,
hub_did, hub_did,
handle, handle,
display_name, display_name,
@ -82,10 +89,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
language, language,
notifications, notifications,
auto_sync, auto_sync,
show_date_of_birth,
} => { } => {
interface.handle_profile_update( interface.handle_profile_update(
&file, &file,
&id, id.as_deref(),
default,
hub_did, hub_did,
handle, handle,
display_name, display_name,
@ -98,6 +107,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
language, language,
notifications, notifications,
auto_sync, auto_sync,
show_date_of_birth,
)?; )?;
} }
crate::cli::commands::ProfileCommands::Delete { file, id } => { crate::cli::commands::ProfileCommands::Delete { file, id } => {