Add universe binding and user profiles to passport
This commit is contained in:
parent
5eea31a25d
commit
af0d66d370
17 changed files with 2494 additions and 54 deletions
147
Cargo.lock
generated
147
Cargo.lock
generated
|
|
@ -116,6 +116,12 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.3"
|
||||
|
|
@ -146,6 +152,33 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"ciborium-ll",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-io"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-ll"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"half 2.7.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
|
|
@ -218,6 +251,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
|
|
@ -363,6 +402,17 @@ version = "1.8.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
|
|
@ -414,6 +464,16 @@ version = "1.70.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.176"
|
||||
|
|
@ -426,6 +486,12 @@ version = "0.11.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
|
|
@ -571,6 +637,12 @@ dependencies = [
|
|||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
|
|
@ -593,7 +665,7 @@ version = "0.11.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
|
||||
dependencies = [
|
||||
"half",
|
||||
"half 1.8.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
|
@ -634,6 +706,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"bip39",
|
||||
"chacha20poly1305",
|
||||
"ciborium",
|
||||
"ed25519-dalek",
|
||||
"hex",
|
||||
"hkdf",
|
||||
|
|
@ -644,6 +717,7 @@ dependencies = [
|
|||
"sha2",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
|
|
@ -657,6 +731,7 @@ dependencies = [
|
|||
"rpassword",
|
||||
"sharenet-passport",
|
||||
"tempfile",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -786,6 +861,17 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
|
|
@ -816,6 +902,65 @@ dependencies = [
|
|||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.0"
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ serde_cbor = "0.11"
|
|||
thiserror = "1.0"
|
||||
zeroize = { version = "1.7", features = ["zeroize_derive"] }
|
||||
hex = "0.4"
|
||||
ciborium = "0.2"
|
||||
uuid = { version = "1.8", features = ["v7"] }
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"] # Support both native and WASM
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use crate::domain::entities::*;
|
|||
use crate::domain::traits::*;
|
||||
use crate::application::error::ApplicationError;
|
||||
use ed25519_dalek::Signer;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub struct CreatePassportUseCase<MG, KD, FE, FS>
|
||||
where
|
||||
|
|
@ -39,6 +40,7 @@ where
|
|||
|
||||
pub fn execute(
|
||||
&self,
|
||||
univ_id: &str,
|
||||
password: &str,
|
||||
output_path: &str,
|
||||
) -> Result<(Passport, RecoveryPhrase), ApplicationError> {
|
||||
|
|
@ -48,10 +50,10 @@ where
|
|||
.generate()
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to generate mnemonic: {}", e.into())))?;
|
||||
|
||||
// Derive seed from mnemonic
|
||||
// Derive seed from mnemonic and universe
|
||||
let seed = self
|
||||
.key_deriver
|
||||
.derive_from_mnemonic(&recovery_phrase)
|
||||
.derive_from_mnemonic(&recovery_phrase, univ_id)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to derive seed: {}", e.into())))?;
|
||||
|
||||
// Derive keys from seed
|
||||
|
|
@ -65,6 +67,7 @@ where
|
|||
seed,
|
||||
public_key,
|
||||
private_key,
|
||||
univ_id.to_string(),
|
||||
);
|
||||
|
||||
// Encrypt and save file
|
||||
|
|
@ -75,6 +78,8 @@ where
|
|||
password,
|
||||
&passport.public_key,
|
||||
&passport.did,
|
||||
&passport.univ_id,
|
||||
&passport.user_profiles,
|
||||
)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
|
||||
|
||||
|
|
@ -122,6 +127,7 @@ where
|
|||
|
||||
pub fn execute(
|
||||
&self,
|
||||
univ_id: &str,
|
||||
recovery_words: &[String],
|
||||
password: &str,
|
||||
output_path: &str,
|
||||
|
|
@ -133,10 +139,10 @@ where
|
|||
|
||||
let recovery_phrase = RecoveryPhrase::new(recovery_words.to_vec());
|
||||
|
||||
// Derive seed from mnemonic
|
||||
// Derive seed from mnemonic and universe
|
||||
let seed = self
|
||||
.key_deriver
|
||||
.derive_from_mnemonic(&recovery_phrase)
|
||||
.derive_from_mnemonic(&recovery_phrase, univ_id)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to derive seed: {}", e.into())))?;
|
||||
|
||||
// Derive keys from seed
|
||||
|
|
@ -150,6 +156,7 @@ where
|
|||
seed,
|
||||
public_key,
|
||||
private_key,
|
||||
univ_id.to_string(),
|
||||
);
|
||||
|
||||
// Encrypt and save file
|
||||
|
|
@ -160,6 +167,8 @@ where
|
|||
password,
|
||||
&passport.public_key,
|
||||
&passport.did,
|
||||
&passport.univ_id,
|
||||
&passport.user_profiles,
|
||||
)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
|
||||
|
||||
|
|
@ -208,18 +217,20 @@ where
|
|||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to load file: {}", e.into())))?;
|
||||
|
||||
// Decrypt file
|
||||
let (seed, public_key, private_key) = self
|
||||
let (seed, public_key, private_key, user_profiles) = self
|
||||
.file_encryptor
|
||||
.decrypt(&passport_file, password)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to decrypt file: {}", e.into())))?;
|
||||
|
||||
|
||||
// Create passport (without storing recovery phrase)
|
||||
let passport = Passport::new(
|
||||
let mut passport = Passport::new(
|
||||
seed,
|
||||
public_key,
|
||||
private_key,
|
||||
passport_file.univ_id.clone(),
|
||||
);
|
||||
passport.user_profiles = user_profiles;
|
||||
|
||||
// Re-encrypt and save if output path provided
|
||||
if let Some(output_path) = output_path {
|
||||
|
|
@ -230,6 +241,8 @@ where
|
|||
password,
|
||||
&passport.public_key,
|
||||
&passport.did,
|
||||
&passport.univ_id,
|
||||
&passport.user_profiles,
|
||||
)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to re-encrypt file: {}", e.into())))?;
|
||||
|
||||
|
|
@ -276,6 +289,8 @@ where
|
|||
password,
|
||||
&passport.public_key,
|
||||
&passport.did,
|
||||
&passport.univ_id,
|
||||
&passport.user_profiles,
|
||||
)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
|
||||
|
||||
|
|
@ -306,10 +321,200 @@ impl SignCardUseCase {
|
|||
.map_err(|_| ApplicationError::UseCaseError("Invalid private key length".to_string()))?
|
||||
);
|
||||
|
||||
// Sign the message
|
||||
let signature = signing_key.sign(message.as_bytes());
|
||||
// Create universe-bound message to sign
|
||||
let message_to_sign = format!("u:{}:{}", passport.univ_id, message);
|
||||
|
||||
// Sign the universe-bound message
|
||||
let signature = signing_key.sign(message_to_sign.as_bytes());
|
||||
|
||||
// Return the signature as bytes
|
||||
Ok(signature.to_bytes().to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateUserProfileUseCase<FE, FS>
|
||||
where
|
||||
FE: FileEncryptor,
|
||||
FS: FileStorage,
|
||||
{
|
||||
file_encryptor: FE,
|
||||
file_storage: FS,
|
||||
}
|
||||
|
||||
impl<FE, FS> CreateUserProfileUseCase<FE, FS>
|
||||
where
|
||||
FE: FileEncryptor,
|
||||
FS: FileStorage,
|
||||
{
|
||||
pub fn new(file_encryptor: FE, file_storage: FS) -> Self {
|
||||
Self {
|
||||
file_encryptor,
|
||||
file_storage,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute(
|
||||
&self,
|
||||
passport: &mut Passport,
|
||||
hub_did: Option<String>,
|
||||
identity: UserIdentity,
|
||||
preferences: UserPreferences,
|
||||
password: &str,
|
||||
file_path: &str,
|
||||
) -> Result<(), ApplicationError> {
|
||||
let profile = UserProfile::new(hub_did, identity, preferences);
|
||||
|
||||
passport.add_user_profile(profile)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to add user profile: {}", e)))?;
|
||||
|
||||
// Save updated passport
|
||||
let passport_file = self
|
||||
.file_encryptor
|
||||
.encrypt(
|
||||
&passport.seed,
|
||||
password,
|
||||
&passport.public_key,
|
||||
&passport.did,
|
||||
&passport.univ_id,
|
||||
&passport.user_profiles,
|
||||
)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
|
||||
|
||||
self.file_storage
|
||||
.save(&passport_file, file_path)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to save file: {}", e.into())))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UpdateUserProfileUseCase<FE, FS>
|
||||
where
|
||||
FE: FileEncryptor,
|
||||
FS: FileStorage,
|
||||
{
|
||||
file_encryptor: FE,
|
||||
file_storage: FS,
|
||||
}
|
||||
|
||||
impl<FE, FS> UpdateUserProfileUseCase<FE, FS>
|
||||
where
|
||||
FE: FileEncryptor,
|
||||
FS: FileStorage,
|
||||
{
|
||||
pub fn new(file_encryptor: FE, file_storage: FS) -> Self {
|
||||
Self {
|
||||
file_encryptor,
|
||||
file_storage,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute(
|
||||
&self,
|
||||
passport: &mut Passport,
|
||||
id: Option<&str>,
|
||||
identity: UserIdentity,
|
||||
preferences: UserPreferences,
|
||||
password: &str,
|
||||
file_path: &str,
|
||||
) -> Result<(), ApplicationError> {
|
||||
// Find existing profile by ID to preserve its ID and created_at
|
||||
let id = id
|
||||
.ok_or_else(|| ApplicationError::UseCaseError("Profile ID is required".to_string()))?;
|
||||
|
||||
let existing_profile = passport.user_profile_by_id(id)
|
||||
.ok_or_else(|| ApplicationError::UseCaseError("User profile not found".to_string()))?;
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Time error: {}", e)))?
|
||||
.as_secs();
|
||||
|
||||
// Use existing hub_did (cannot change hub_did via update)
|
||||
let profile = UserProfile {
|
||||
id: existing_profile.id.clone(),
|
||||
hub_did: existing_profile.hub_did.clone(),
|
||||
identity,
|
||||
preferences,
|
||||
created_at: existing_profile.created_at,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
passport.update_user_profile_by_id(id, profile)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to update user profile: {}", e)))?;
|
||||
|
||||
// Save updated passport
|
||||
let passport_file = self
|
||||
.file_encryptor
|
||||
.encrypt(
|
||||
&passport.seed,
|
||||
password,
|
||||
&passport.public_key,
|
||||
&passport.did,
|
||||
&passport.univ_id,
|
||||
&passport.user_profiles,
|
||||
)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
|
||||
|
||||
self.file_storage
|
||||
.save(&passport_file, file_path)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to save file: {}", e.into())))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeleteUserProfileUseCase<FE, FS>
|
||||
where
|
||||
FE: FileEncryptor,
|
||||
FS: FileStorage,
|
||||
{
|
||||
file_encryptor: FE,
|
||||
file_storage: FS,
|
||||
}
|
||||
|
||||
impl<FE, FS> DeleteUserProfileUseCase<FE, FS>
|
||||
where
|
||||
FE: FileEncryptor,
|
||||
FS: FileStorage,
|
||||
{
|
||||
pub fn new(file_encryptor: FE, file_storage: FS) -> Self {
|
||||
Self {
|
||||
file_encryptor,
|
||||
file_storage,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute(
|
||||
&self,
|
||||
passport: &mut Passport,
|
||||
id: Option<&str>,
|
||||
password: &str,
|
||||
file_path: &str,
|
||||
) -> Result<(), ApplicationError> {
|
||||
let id = id
|
||||
.ok_or_else(|| ApplicationError::UseCaseError("Profile ID is required".to_string()))?;
|
||||
|
||||
passport.remove_user_profile_by_id(id)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to remove user profile: {}", e)))?;
|
||||
|
||||
// Save updated passport
|
||||
let passport_file = self
|
||||
.file_encryptor
|
||||
.encrypt(
|
||||
&passport.seed,
|
||||
password,
|
||||
&passport.public_key,
|
||||
&passport.did,
|
||||
&passport.univ_id,
|
||||
&passport.user_profiles,
|
||||
)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
|
||||
|
||||
self.file_storage
|
||||
.save(&passport_file, file_path)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to save file: {}", e.into())))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::application::use_cases::{CreatePassportUseCase, ImportFromRecoveryUseCase, ImportFromFileUseCase, ExportPassportUseCase, SignCardUseCase};
|
||||
use crate::application::use_cases::{CreatePassportUseCase, ImportFromRecoveryUseCase, ImportFromFileUseCase, ExportPassportUseCase, SignCardUseCase, CreateUserProfileUseCase, UpdateUserProfileUseCase, DeleteUserProfileUseCase};
|
||||
// Note: These domain entities are used indirectly through the use cases
|
||||
use crate::infrastructure::crypto::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor};
|
||||
use crate::infrastructure::storage::FileSystemStorage;
|
||||
|
|
@ -19,12 +19,15 @@ mod tests {
|
|||
let file_path = temp_file.path().to_str().unwrap();
|
||||
let password = "test-password";
|
||||
|
||||
let (passport, recovery_phrase) = use_case.execute(password, file_path).unwrap();
|
||||
let (passport, recovery_phrase) = use_case.execute("univ:test:create", password, file_path).unwrap();
|
||||
|
||||
// Verify passport structure
|
||||
assert_eq!(recovery_phrase.words().len(), 24);
|
||||
assert_eq!(passport.public_key().0.len(), 32);
|
||||
assert!(passport.did().as_str().starts_with("did:sharenet:"));
|
||||
// DID should have "p:" prefix followed by hex-encoded public key (66 characters for 32 bytes + "p:")
|
||||
assert_eq!(passport.did().as_str().len(), 66);
|
||||
assert!(passport.did().as_str().starts_with("p:"));
|
||||
assert!(passport.did().as_str().chars().skip(2).all(|c| c.is_ascii_hexdigit()));
|
||||
|
||||
// Verify file was created
|
||||
assert!(std::path::Path::new(file_path).exists());
|
||||
|
|
@ -44,7 +47,7 @@ mod tests {
|
|||
let file_path1 = temp_file1.path().to_str().unwrap();
|
||||
let password = "test-password";
|
||||
|
||||
let (passport, recovery_phrase) = create_use_case.execute(password, file_path1).unwrap();
|
||||
let (passport, recovery_phrase) = create_use_case.execute("univ:test:create", password, file_path1).unwrap();
|
||||
let original_did = passport.did().as_str().to_string();
|
||||
|
||||
// Now import from the recovery phrase
|
||||
|
|
@ -59,6 +62,7 @@ mod tests {
|
|||
let file_path2 = temp_file2.path().to_str().unwrap();
|
||||
|
||||
let imported_passport = import_use_case.execute(
|
||||
"univ:test:create",
|
||||
recovery_phrase.words(),
|
||||
password,
|
||||
file_path2,
|
||||
|
|
@ -83,7 +87,7 @@ mod tests {
|
|||
let file_path1 = temp_file1.path().to_str().unwrap();
|
||||
let password = "test-password";
|
||||
|
||||
let (passport, _) = create_use_case.execute(password, file_path1).unwrap();
|
||||
let (passport, _) = create_use_case.execute("univ:test:create", password, file_path1).unwrap();
|
||||
let original_did = passport.did().as_str().to_string();
|
||||
|
||||
// Now import from the file
|
||||
|
|
@ -117,7 +121,7 @@ mod tests {
|
|||
let file_path1 = temp_file1.path().to_str().unwrap();
|
||||
let password = "test-password";
|
||||
|
||||
let (passport, _) = create_use_case.execute(password, file_path1).unwrap();
|
||||
let (passport, _) = create_use_case.execute("univ:test:create", password, file_path1).unwrap();
|
||||
let original_did = passport.did().as_str().to_string();
|
||||
|
||||
// Now import and re-encrypt to a new file
|
||||
|
|
@ -157,7 +161,7 @@ mod tests {
|
|||
let file_path1 = temp_file1.path().to_str().unwrap();
|
||||
let password = "test-password";
|
||||
|
||||
let (passport, _) = create_use_case.execute(password, file_path1).unwrap();
|
||||
let (passport, _) = create_use_case.execute("univ:test:create", password, file_path1).unwrap();
|
||||
let original_did = passport.did().as_str().to_string();
|
||||
|
||||
// Now export with a new password
|
||||
|
|
@ -204,7 +208,7 @@ mod tests {
|
|||
let file_path = temp_file.path().to_str().unwrap();
|
||||
let password = "test-password";
|
||||
|
||||
create_use_case.execute(password, file_path).unwrap();
|
||||
create_use_case.execute("univ:test:create", password, file_path).unwrap();
|
||||
|
||||
// Try to import with wrong password
|
||||
let import_use_case = ImportFromFileUseCase::new(
|
||||
|
|
@ -237,7 +241,7 @@ mod tests {
|
|||
|
||||
let invalid_words = vec!["invalid".to_string(); 24];
|
||||
|
||||
let result = use_case.execute(&invalid_words, password, file_path);
|
||||
let result = use_case.execute("univ:test:create", &invalid_words, password, file_path);
|
||||
|
||||
// Should fail due to invalid mnemonic
|
||||
assert!(result.is_err());
|
||||
|
|
@ -259,7 +263,7 @@ mod tests {
|
|||
let file_path = temp_file.path().to_str().unwrap();
|
||||
let password = "test-password";
|
||||
|
||||
let (passport, _) = create_use_case.execute(password, file_path).unwrap();
|
||||
let (passport, _) = create_use_case.execute("univ:test:create", password, file_path).unwrap();
|
||||
|
||||
// Sign a test message
|
||||
let test_message = "Hello, Sharenet!";
|
||||
|
|
@ -273,8 +277,10 @@ mod tests {
|
|||
&passport.public_key().0[..].try_into().unwrap()
|
||||
).unwrap();
|
||||
|
||||
// Create the universe-bound message that was actually signed
|
||||
let universe_bound_message = format!("u:{}:{}", passport.univ_id(), test_message);
|
||||
let signature_obj = ed25519_dalek::Signature::from_bytes(&signature.try_into().unwrap());
|
||||
let verification_result = verifying_key.verify_strict(test_message.as_bytes(), &signature_obj);
|
||||
let verification_result = verifying_key.verify_strict(universe_bound_message.as_bytes(), &signature_obj);
|
||||
|
||||
assert!(verification_result.is_ok());
|
||||
}
|
||||
|
|
@ -295,7 +301,7 @@ mod tests {
|
|||
let file_path = temp_file.path().to_str().unwrap();
|
||||
let password = "test-password";
|
||||
|
||||
let (passport, _) = create_use_case.execute(password, file_path).unwrap();
|
||||
let (passport, _) = create_use_case.execute("univ:test:create", password, file_path).unwrap();
|
||||
|
||||
// Sign two different messages
|
||||
let message1 = "Message 1";
|
||||
|
|
@ -311,4 +317,607 @@ mod tests {
|
|||
assert_eq!(signature1.len(), 64);
|
||||
assert_eq!(signature2.len(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_profile_management_integration() {
|
||||
use crate::application::use_cases::{CreateUserProfileUseCase, UpdateUserProfileUseCase, DeleteUserProfileUseCase};
|
||||
use crate::domain::entities::{UserIdentity, UserPreferences};
|
||||
|
||||
// First create a passport
|
||||
let create_use_case = CreatePassportUseCase::new(
|
||||
Bip39MnemonicGenerator,
|
||||
Ed25519KeyDeriver,
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let file_path = temp_file.path().to_str().unwrap();
|
||||
let password = "test-password";
|
||||
|
||||
let (mut passport, _) = create_use_case.execute("univ:test:profiles", password, file_path).unwrap();
|
||||
|
||||
// Verify default profile exists
|
||||
assert!(passport.default_user_profile().is_some());
|
||||
assert_eq!(passport.user_profiles().len(), 1);
|
||||
|
||||
// Create user profile use cases
|
||||
let create_profile_use_case = CreateUserProfileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
let update_profile_use_case = UpdateUserProfileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
let delete_profile_use_case = DeleteUserProfileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
// Create a hub-specific profile
|
||||
let hub_identity = UserIdentity {
|
||||
handle: Some("hubuser".to_string()),
|
||||
display_name: Some("Hub User".to_string()),
|
||||
first_name: Some("Hub".to_string()),
|
||||
last_name: Some("User".to_string()),
|
||||
email: Some("hub@example.com".to_string()),
|
||||
avatar_url: Some("https://example.com/avatar.png".to_string()),
|
||||
bio: Some("Hub user bio".to_string()),
|
||||
};
|
||||
|
||||
let hub_preferences = UserPreferences {
|
||||
theme: Some("light".to_string()),
|
||||
language: Some("fr".to_string()),
|
||||
notifications_enabled: false,
|
||||
auto_sync: true,
|
||||
};
|
||||
|
||||
create_profile_use_case.execute(
|
||||
&mut passport,
|
||||
Some("h:hub1".to_string()),
|
||||
hub_identity,
|
||||
hub_preferences,
|
||||
password,
|
||||
file_path,
|
||||
).unwrap();
|
||||
|
||||
// Verify profile was added
|
||||
assert_eq!(passport.user_profiles().len(), 2);
|
||||
let hub_profile = passport.user_profile_for_hub("h:hub1").unwrap();
|
||||
assert_eq!(hub_profile.identity.handle, Some("hubuser".to_string()));
|
||||
assert_eq!(hub_profile.identity.display_name, Some("Hub User".to_string()));
|
||||
assert_eq!(hub_profile.preferences.language, Some("fr".to_string()));
|
||||
|
||||
// Update the hub profile
|
||||
let updated_identity = UserIdentity {
|
||||
handle: Some("updatedhubuser".to_string()),
|
||||
display_name: Some("Updated Hub User".to_string()),
|
||||
first_name: Some("Updated".to_string()),
|
||||
last_name: Some("Hub User".to_string()),
|
||||
email: Some("updated@example.com".to_string()),
|
||||
avatar_url: Some("https://example.com/new-avatar.png".to_string()),
|
||||
bio: Some("Updated bio".to_string()),
|
||||
};
|
||||
|
||||
let updated_preferences = UserPreferences {
|
||||
theme: Some("dark".to_string()),
|
||||
language: Some("en".to_string()),
|
||||
notifications_enabled: true,
|
||||
auto_sync: false,
|
||||
};
|
||||
|
||||
// Get the profile ID for the hub profile
|
||||
let profile_id = passport.user_profile_for_hub("h:hub1").unwrap().id.clone();
|
||||
|
||||
update_profile_use_case.execute(
|
||||
&mut passport,
|
||||
Some(&profile_id),
|
||||
updated_identity,
|
||||
updated_preferences,
|
||||
password,
|
||||
file_path,
|
||||
).unwrap();
|
||||
|
||||
// Verify profile was updated
|
||||
let updated_profile = passport.user_profile_for_hub("h:hub1").unwrap();
|
||||
assert_eq!(updated_profile.identity.handle, Some("updatedhubuser".to_string()));
|
||||
assert_eq!(updated_profile.identity.display_name, Some("Updated Hub User".to_string()));
|
||||
assert_eq!(updated_profile.preferences.theme, Some("dark".to_string()));
|
||||
assert_eq!(updated_profile.preferences.language, Some("en".to_string()));
|
||||
|
||||
// Delete the hub profile
|
||||
let profile_id = passport.user_profile_for_hub("h:hub1").unwrap().id.clone();
|
||||
|
||||
delete_profile_use_case.execute(
|
||||
&mut passport,
|
||||
Some(&profile_id),
|
||||
password,
|
||||
file_path,
|
||||
).unwrap();
|
||||
|
||||
// Verify profile was removed
|
||||
assert_eq!(passport.user_profiles().len(), 1);
|
||||
assert!(passport.user_profile_for_hub("h:hub1").is_none());
|
||||
|
||||
// Verify default profile still exists
|
||||
assert!(passport.default_user_profile().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_profile_persistence_across_import() {
|
||||
use crate::domain::entities::{UserIdentity, UserPreferences};
|
||||
|
||||
// First create a passport with user profiles
|
||||
let create_use_case = CreatePassportUseCase::new(
|
||||
Bip39MnemonicGenerator,
|
||||
Ed25519KeyDeriver,
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let file_path = temp_file.path().to_str().unwrap();
|
||||
let password = "test-password";
|
||||
|
||||
let (mut passport, _) = create_use_case.execute("univ:test:persistence", password, file_path).unwrap();
|
||||
|
||||
// Add a hub profile
|
||||
let create_profile_use_case = CreateUserProfileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
let hub_identity = UserIdentity {
|
||||
handle: Some("persistentuser".to_string()),
|
||||
display_name: Some("Persistent User".to_string()),
|
||||
first_name: Some("Persistent".to_string()),
|
||||
last_name: Some("User".to_string()),
|
||||
email: Some("persistent@example.com".to_string()),
|
||||
avatar_url: None,
|
||||
bio: Some("This should persist".to_string()),
|
||||
};
|
||||
|
||||
let hub_preferences = UserPreferences {
|
||||
theme: Some("auto".to_string()),
|
||||
language: Some("es".to_string()),
|
||||
notifications_enabled: true,
|
||||
auto_sync: true,
|
||||
};
|
||||
|
||||
create_profile_use_case.execute(
|
||||
&mut passport,
|
||||
Some("h:persistence".to_string()),
|
||||
hub_identity,
|
||||
hub_preferences,
|
||||
password,
|
||||
file_path,
|
||||
).unwrap();
|
||||
|
||||
// Now import from the file
|
||||
let import_use_case = ImportFromFileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
let imported_passport = import_use_case.execute(
|
||||
file_path,
|
||||
password,
|
||||
None,
|
||||
).unwrap();
|
||||
|
||||
// Verify user profiles persisted
|
||||
assert_eq!(imported_passport.user_profiles().len(), 2);
|
||||
|
||||
// Verify default profile
|
||||
assert!(imported_passport.default_user_profile().is_some());
|
||||
|
||||
// Verify hub profile
|
||||
let imported_hub_profile = imported_passport.user_profile_for_hub("h:persistence").unwrap();
|
||||
assert_eq!(imported_hub_profile.identity.handle, Some("persistentuser".to_string()));
|
||||
assert_eq!(imported_hub_profile.identity.display_name, Some("Persistent User".to_string()));
|
||||
assert_eq!(imported_hub_profile.identity.email, Some("persistent@example.com".to_string()));
|
||||
assert_eq!(imported_hub_profile.preferences.language, Some("es".to_string()));
|
||||
assert_eq!(imported_hub_profile.preferences.theme, Some("auto".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_user_profile_use_case() {
|
||||
use crate::domain::entities::{UserIdentity, UserPreferences};
|
||||
|
||||
// First create a passport
|
||||
let create_use_case = CreatePassportUseCase::new(
|
||||
Bip39MnemonicGenerator,
|
||||
Ed25519KeyDeriver,
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let file_path = temp_file.path().to_str().unwrap();
|
||||
let password = "test-password";
|
||||
|
||||
let (mut passport, _) = create_use_case.execute("univ:test:update", password, file_path).unwrap();
|
||||
|
||||
// Add a hub profile to update
|
||||
let create_profile_use_case = CreateUserProfileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
let original_identity = UserIdentity {
|
||||
handle: Some("originaluser".to_string()),
|
||||
display_name: Some("Original User".to_string()),
|
||||
first_name: Some("Original".to_string()),
|
||||
last_name: Some("User".to_string()),
|
||||
email: Some("original@example.com".to_string()),
|
||||
avatar_url: None,
|
||||
bio: Some("Original bio".to_string()),
|
||||
};
|
||||
|
||||
let original_preferences = UserPreferences {
|
||||
theme: Some("light".to_string()),
|
||||
language: Some("en".to_string()),
|
||||
notifications_enabled: false,
|
||||
auto_sync: false,
|
||||
};
|
||||
|
||||
create_profile_use_case.execute(
|
||||
&mut passport,
|
||||
Some("h:update".to_string()),
|
||||
original_identity,
|
||||
original_preferences,
|
||||
password,
|
||||
file_path,
|
||||
).unwrap();
|
||||
|
||||
// Get the profile ID
|
||||
let profile_id = passport.user_profile_for_hub("h:update").unwrap().id.clone();
|
||||
|
||||
// Create update use case
|
||||
let update_profile_use_case = UpdateUserProfileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
// Update the profile
|
||||
let updated_identity = UserIdentity {
|
||||
handle: Some("updateduser".to_string()),
|
||||
display_name: Some("Updated User".to_string()),
|
||||
first_name: Some("Updated".to_string()),
|
||||
last_name: Some("User".to_string()),
|
||||
email: Some("updated@example.com".to_string()),
|
||||
avatar_url: Some("https://example.com/new-avatar.png".to_string()),
|
||||
bio: Some("Updated bio".to_string()),
|
||||
};
|
||||
|
||||
let updated_preferences = UserPreferences {
|
||||
theme: Some("dark".to_string()),
|
||||
language: Some("fr".to_string()),
|
||||
notifications_enabled: true,
|
||||
auto_sync: true,
|
||||
};
|
||||
|
||||
update_profile_use_case.execute(
|
||||
&mut passport,
|
||||
Some(&profile_id),
|
||||
updated_identity,
|
||||
updated_preferences,
|
||||
password,
|
||||
file_path,
|
||||
).unwrap();
|
||||
|
||||
// Verify the profile was updated
|
||||
let updated_profile = passport.user_profile_for_hub("h:update").unwrap();
|
||||
assert_eq!(updated_profile.identity.handle, Some("updateduser".to_string()));
|
||||
assert_eq!(updated_profile.identity.display_name, Some("Updated User".to_string()));
|
||||
assert_eq!(updated_profile.identity.email, Some("updated@example.com".to_string()));
|
||||
assert_eq!(updated_profile.preferences.theme, Some("dark".to_string()));
|
||||
assert_eq!(updated_profile.preferences.language, Some("fr".to_string()));
|
||||
assert!(updated_profile.preferences.notifications_enabled);
|
||||
assert!(updated_profile.preferences.auto_sync);
|
||||
|
||||
// Verify the ID and hub_did remain the same
|
||||
assert_eq!(updated_profile.id, profile_id);
|
||||
assert_eq!(updated_profile.hub_did, Some("h:update".to_string()));
|
||||
|
||||
// Verify updated_at timestamp changed
|
||||
assert!(updated_profile.updated_at > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_user_profile_use_case_invalid_id() {
|
||||
use crate::domain::entities::{UserIdentity, UserPreferences};
|
||||
|
||||
// First create a passport
|
||||
let create_use_case = CreatePassportUseCase::new(
|
||||
Bip39MnemonicGenerator,
|
||||
Ed25519KeyDeriver,
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let file_path = temp_file.path().to_str().unwrap();
|
||||
let password = "test-password";
|
||||
|
||||
let (mut passport, _) = create_use_case.execute("univ:test:update_invalid", password, file_path).unwrap();
|
||||
|
||||
// Create update use case
|
||||
let update_profile_use_case = UpdateUserProfileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
// Try to update with invalid ID
|
||||
let identity = UserIdentity {
|
||||
handle: Some("test".to_string()),
|
||||
display_name: Some("Test".to_string()),
|
||||
first_name: Some("Test".to_string()),
|
||||
last_name: Some("User".to_string()),
|
||||
email: Some("test@example.com".to_string()),
|
||||
avatar_url: None,
|
||||
bio: None,
|
||||
};
|
||||
|
||||
let preferences = UserPreferences {
|
||||
theme: Some("dark".to_string()),
|
||||
language: Some("en".to_string()),
|
||||
notifications_enabled: true,
|
||||
auto_sync: false,
|
||||
};
|
||||
|
||||
let result = update_profile_use_case.execute(
|
||||
&mut passport,
|
||||
Some("invalid-uuid"),
|
||||
identity,
|
||||
preferences,
|
||||
password,
|
||||
file_path,
|
||||
);
|
||||
|
||||
// Should fail with profile not found
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_user_profile_use_case_missing_id() {
|
||||
use crate::domain::entities::{UserIdentity, UserPreferences};
|
||||
|
||||
// First create a passport
|
||||
let create_use_case = CreatePassportUseCase::new(
|
||||
Bip39MnemonicGenerator,
|
||||
Ed25519KeyDeriver,
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let file_path = temp_file.path().to_str().unwrap();
|
||||
let password = "test-password";
|
||||
|
||||
let (mut passport, _) = create_use_case.execute("univ:test:update_missing", password, file_path).unwrap();
|
||||
|
||||
// Create update use case
|
||||
let update_profile_use_case = UpdateUserProfileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
// Try to update with missing ID
|
||||
let identity = UserIdentity {
|
||||
handle: Some("test".to_string()),
|
||||
display_name: Some("Test".to_string()),
|
||||
first_name: Some("Test".to_string()),
|
||||
last_name: Some("User".to_string()),
|
||||
email: Some("test@example.com".to_string()),
|
||||
avatar_url: None,
|
||||
bio: None,
|
||||
};
|
||||
|
||||
let preferences = UserPreferences {
|
||||
theme: Some("dark".to_string()),
|
||||
language: Some("en".to_string()),
|
||||
notifications_enabled: true,
|
||||
auto_sync: false,
|
||||
};
|
||||
|
||||
let result = update_profile_use_case.execute(
|
||||
&mut passport,
|
||||
None,
|
||||
identity,
|
||||
preferences,
|
||||
password,
|
||||
file_path,
|
||||
);
|
||||
|
||||
// Should fail with ID required
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_user_profile_use_case() {
|
||||
use crate::domain::entities::{UserIdentity, UserPreferences};
|
||||
|
||||
// First create a passport
|
||||
let create_use_case = CreatePassportUseCase::new(
|
||||
Bip39MnemonicGenerator,
|
||||
Ed25519KeyDeriver,
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let file_path = temp_file.path().to_str().unwrap();
|
||||
let password = "test-password";
|
||||
|
||||
let (mut passport, _) = create_use_case.execute("univ:test:delete", password, file_path).unwrap();
|
||||
|
||||
// Add a hub profile to delete
|
||||
let create_profile_use_case = CreateUserProfileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
let identity = UserIdentity {
|
||||
handle: Some("todelete".to_string()),
|
||||
display_name: Some("To Delete".to_string()),
|
||||
first_name: Some("To".to_string()),
|
||||
last_name: Some("Delete".to_string()),
|
||||
email: Some("delete@example.com".to_string()),
|
||||
avatar_url: None,
|
||||
bio: Some("This will be deleted".to_string()),
|
||||
};
|
||||
|
||||
let preferences = UserPreferences {
|
||||
theme: Some("light".to_string()),
|
||||
language: Some("en".to_string()),
|
||||
notifications_enabled: false,
|
||||
auto_sync: false,
|
||||
};
|
||||
|
||||
create_profile_use_case.execute(
|
||||
&mut passport,
|
||||
Some("h:delete".to_string()),
|
||||
identity,
|
||||
preferences,
|
||||
password,
|
||||
file_path,
|
||||
).unwrap();
|
||||
|
||||
// Verify profile was added
|
||||
assert_eq!(passport.user_profiles().len(), 2);
|
||||
assert!(passport.user_profile_for_hub("h:delete").is_some());
|
||||
|
||||
// Get the profile ID
|
||||
let profile_id = passport.user_profile_for_hub("h:delete").unwrap().id.clone();
|
||||
|
||||
// Create delete use case
|
||||
let delete_profile_use_case = DeleteUserProfileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
// Delete the profile
|
||||
delete_profile_use_case.execute(
|
||||
&mut passport,
|
||||
Some(&profile_id),
|
||||
password,
|
||||
file_path,
|
||||
).unwrap();
|
||||
|
||||
// Verify profile was deleted
|
||||
assert_eq!(passport.user_profiles().len(), 1);
|
||||
assert!(passport.user_profile_for_hub("h:delete").is_none());
|
||||
|
||||
// Verify default profile still exists
|
||||
assert!(passport.default_user_profile().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_user_profile_use_case_invalid_id() {
|
||||
// First create a passport
|
||||
let create_use_case = CreatePassportUseCase::new(
|
||||
Bip39MnemonicGenerator,
|
||||
Ed25519KeyDeriver,
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let file_path = temp_file.path().to_str().unwrap();
|
||||
let password = "test-password";
|
||||
|
||||
let (mut passport, _) = create_use_case.execute("univ:test:delete_invalid", password, file_path).unwrap();
|
||||
|
||||
// Create delete use case
|
||||
let delete_profile_use_case = DeleteUserProfileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
// Try to delete with invalid ID
|
||||
let result = delete_profile_use_case.execute(
|
||||
&mut passport,
|
||||
Some("invalid-uuid"),
|
||||
password,
|
||||
file_path,
|
||||
);
|
||||
|
||||
// Should fail with profile not found
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_user_profile_use_case_missing_id() {
|
||||
// First create a passport
|
||||
let create_use_case = CreatePassportUseCase::new(
|
||||
Bip39MnemonicGenerator,
|
||||
Ed25519KeyDeriver,
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let file_path = temp_file.path().to_str().unwrap();
|
||||
let password = "test-password";
|
||||
|
||||
let (mut passport, _) = create_use_case.execute("univ:test:delete_missing", password, file_path).unwrap();
|
||||
|
||||
// Create delete use case
|
||||
let delete_profile_use_case = DeleteUserProfileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
// Try to delete with missing ID
|
||||
let result = delete_profile_use_case.execute(
|
||||
&mut passport,
|
||||
None,
|
||||
password,
|
||||
file_path,
|
||||
);
|
||||
|
||||
// Should fail with ID required
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_user_profile_use_case_cannot_delete_default() {
|
||||
// First create a passport
|
||||
let create_use_case = CreatePassportUseCase::new(
|
||||
Bip39MnemonicGenerator,
|
||||
Ed25519KeyDeriver,
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let file_path = temp_file.path().to_str().unwrap();
|
||||
let password = "test-password";
|
||||
|
||||
let (mut passport, _) = create_use_case.execute("univ:test:delete_default", password, file_path).unwrap();
|
||||
|
||||
// Get the default profile ID
|
||||
let default_profile_id = passport.default_user_profile().unwrap().id.clone();
|
||||
|
||||
// Create delete use case
|
||||
let delete_profile_use_case = DeleteUserProfileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
// Try to delete the default profile
|
||||
let result = delete_profile_use_case.execute(
|
||||
&mut passport,
|
||||
Some(&default_profile_id),
|
||||
password,
|
||||
file_path,
|
||||
);
|
||||
|
||||
// Should fail - cannot delete default profile
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use uuid::Uuid;
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -43,8 +45,8 @@ pub struct Did(pub String);
|
|||
|
||||
impl Did {
|
||||
pub fn new(public_key: &PublicKey) -> Self {
|
||||
// Simple DID format for now - in production this would use proper DID method
|
||||
let did_str = format!("did:sharenet:{}", hex::encode(&public_key.0));
|
||||
// Passport DID format with "p:" prefix
|
||||
let did_str = format!("p:{}", hex::encode(&public_key.0));
|
||||
Self(did_str)
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +76,8 @@ pub struct Passport {
|
|||
pub public_key: PublicKey,
|
||||
pub private_key: PrivateKey,
|
||||
pub did: Did,
|
||||
pub univ_id: String,
|
||||
pub user_profiles: Vec<UserProfile>,
|
||||
}
|
||||
|
||||
impl Passport {
|
||||
|
|
@ -81,13 +85,37 @@ impl Passport {
|
|||
seed: Seed,
|
||||
public_key: PublicKey,
|
||||
private_key: PrivateKey,
|
||||
univ_id: String,
|
||||
) -> Self {
|
||||
let did = Did::new(&public_key);
|
||||
|
||||
// Create default user profile
|
||||
let default_profile = UserProfile::new(
|
||||
None,
|
||||
UserIdentity {
|
||||
handle: None,
|
||||
display_name: None,
|
||||
first_name: None,
|
||||
last_name: None,
|
||||
email: None,
|
||||
avatar_url: None,
|
||||
bio: None,
|
||||
},
|
||||
UserPreferences {
|
||||
theme: None,
|
||||
language: None,
|
||||
notifications_enabled: true,
|
||||
auto_sync: true,
|
||||
},
|
||||
);
|
||||
|
||||
Self {
|
||||
seed,
|
||||
public_key,
|
||||
private_key,
|
||||
did,
|
||||
univ_id,
|
||||
user_profiles: vec![default_profile],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,6 +126,164 @@ impl Passport {
|
|||
pub fn did(&self) -> &Did {
|
||||
&self.did
|
||||
}
|
||||
|
||||
pub fn univ_id(&self) -> &str {
|
||||
&self.univ_id
|
||||
}
|
||||
|
||||
pub fn user_profiles(&self) -> &[UserProfile] {
|
||||
&self.user_profiles
|
||||
}
|
||||
|
||||
pub fn default_user_profile(&self) -> Option<&UserProfile> {
|
||||
self.user_profiles.iter().find(|p| p.is_default())
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
pub fn user_profile_by_id(&self, profile_id: &str) -> Option<&UserProfile> {
|
||||
self.user_profiles.iter().find(|p| p.id == profile_id)
|
||||
}
|
||||
|
||||
pub fn user_profile_by_id_mut(&mut self, profile_id: &str) -> Option<&mut UserProfile> {
|
||||
self.user_profiles.iter_mut().find(|p| p.id == profile_id)
|
||||
}
|
||||
|
||||
pub fn add_user_profile(&mut self, profile: UserProfile) -> Result<(), String> {
|
||||
// Ensure only one default profile
|
||||
if profile.is_default() && self.default_user_profile().is_some() {
|
||||
return Err("Default user profile already exists".to_string());
|
||||
}
|
||||
|
||||
// Ensure hub_did is unique
|
||||
if let Some(hub_did) = &profile.hub_did {
|
||||
if self.user_profile_for_hub(hub_did).is_some() {
|
||||
return Err(format!("User profile for hub DID {} already exists", hub_did));
|
||||
}
|
||||
}
|
||||
|
||||
self.user_profiles.push(profile);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_user_profile(&mut self, hub_did: Option<&str>, profile: UserProfile) -> Result<(), String> {
|
||||
let index = self.user_profiles.iter().position(|p| {
|
||||
match (p.hub_did.as_deref(), hub_did) {
|
||||
(None, None) => true, // Default profile
|
||||
(Some(p_hub), Some(hub)) if p_hub == hub => true, // Hub-specific profile
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
|
||||
match index {
|
||||
Some(idx) => {
|
||||
self.user_profiles[idx] = profile;
|
||||
Ok(())
|
||||
}
|
||||
None => Err("User profile not found".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_user_profile(&mut self, hub_did: Option<&str>) -> Result<(), String> {
|
||||
if hub_did.is_none() {
|
||||
return Err("Cannot delete default user profile".to_string());
|
||||
}
|
||||
|
||||
let index = self.user_profiles.iter().position(|p| p.hub_did.as_deref() == hub_did);
|
||||
|
||||
match index {
|
||||
Some(idx) => {
|
||||
self.user_profiles.remove(idx);
|
||||
Ok(())
|
||||
}
|
||||
None => Err("User profile not found".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_user_profile_by_id(&mut self, profile_id: &str, profile: UserProfile) -> Result<(), String> {
|
||||
let index = self.user_profiles.iter().position(|p| p.id == profile_id);
|
||||
|
||||
match index {
|
||||
Some(idx) => {
|
||||
self.user_profiles[idx] = profile;
|
||||
Ok(())
|
||||
}
|
||||
None => Err("User profile not found".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_user_profile_by_id(&mut self, profile_id: &str) -> Result<(), String> {
|
||||
let index = self.user_profiles.iter().position(|p| p.id == profile_id);
|
||||
|
||||
match index {
|
||||
Some(idx) => {
|
||||
// Check if this is the default profile
|
||||
if self.user_profiles[idx].is_default() {
|
||||
return Err("Cannot delete default user profile".to_string());
|
||||
}
|
||||
self.user_profiles.remove(idx);
|
||||
Ok(())
|
||||
}
|
||||
None => Err("User profile not found".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserIdentity {
|
||||
pub handle: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
pub first_name: Option<String>,
|
||||
pub last_name: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserPreferences {
|
||||
pub theme: Option<String>,
|
||||
pub language: Option<String>,
|
||||
pub notifications_enabled: bool,
|
||||
pub auto_sync: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserProfile {
|
||||
pub id: String, // UUIDv7 unique identifier for the profile
|
||||
pub hub_did: Option<String>, // None for default profile
|
||||
pub identity: UserIdentity,
|
||||
pub preferences: UserPreferences,
|
||||
pub created_at: u64,
|
||||
pub updated_at: u64,
|
||||
}
|
||||
|
||||
impl UserProfile {
|
||||
pub fn new(
|
||||
hub_did: Option<String>,
|
||||
identity: UserIdentity,
|
||||
preferences: UserPreferences,
|
||||
) -> Self {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
Self {
|
||||
id: Uuid::now_v7().to_string(),
|
||||
hub_did,
|
||||
identity,
|
||||
preferences,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_default(&self) -> bool {
|
||||
self.hub_did.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
|
@ -109,6 +295,8 @@ pub struct PassportFile {
|
|||
pub nonce: Vec<u8>,
|
||||
pub public_key: Vec<u8>,
|
||||
pub did: String,
|
||||
pub univ_id: String,
|
||||
pub created_at: u64,
|
||||
pub version: String,
|
||||
pub enc_user_profiles: Vec<u8>, // Encrypted CBOR of Vec<UserProfile>
|
||||
}
|
||||
|
|
@ -21,8 +21,13 @@ mod tests {
|
|||
let public_key = PublicKey(vec![1, 2, 3, 4, 5]);
|
||||
let did = Did::new(&public_key);
|
||||
|
||||
assert!(did.as_str().starts_with("did:sharenet:"));
|
||||
assert!(did.as_str().contains(&hex::encode(&public_key.0)));
|
||||
// DID should have "p:" prefix followed by hex-encoded public key
|
||||
let expected_did = format!("p:{}", hex::encode(&public_key.0));
|
||||
assert_eq!(did.as_str(), expected_did);
|
||||
// For a 5-byte public key, hex encoding should be 10 characters + "p:" prefix = 12 characters
|
||||
assert_eq!(did.as_str().len(), 12);
|
||||
assert!(did.as_str().starts_with("p:"));
|
||||
assert!(did.as_str().chars().skip(2).all(|c| c.is_ascii_hexdigit()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -50,4 +55,194 @@ mod tests {
|
|||
// After zeroization, bytes should be empty (zeroize clears the vector)
|
||||
assert_eq!(private_key.0, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_profile_creation() {
|
||||
use crate::domain::entities::{UserIdentity, UserPreferences, UserProfile};
|
||||
|
||||
let identity = UserIdentity {
|
||||
handle: Some("testuser".to_string()),
|
||||
display_name: Some("Test User".to_string()),
|
||||
first_name: Some("Test".to_string()),
|
||||
last_name: Some("User".to_string()),
|
||||
email: Some("test@example.com".to_string()),
|
||||
avatar_url: Some("https://example.com/avatar.png".to_string()),
|
||||
bio: Some("Test bio".to_string()),
|
||||
};
|
||||
|
||||
let preferences = UserPreferences {
|
||||
theme: Some("dark".to_string()),
|
||||
language: Some("en".to_string()),
|
||||
notifications_enabled: true,
|
||||
auto_sync: false,
|
||||
};
|
||||
|
||||
let profile = UserProfile {
|
||||
id: "test-uuid-1234".to_string(),
|
||||
hub_did: Some("h:example".to_string()),
|
||||
identity,
|
||||
preferences,
|
||||
created_at: 1234567890,
|
||||
updated_at: 1234567890,
|
||||
};
|
||||
|
||||
assert_eq!(profile.hub_did, Some("h:example".to_string()));
|
||||
assert_eq!(profile.identity.handle, Some("testuser".to_string()));
|
||||
assert_eq!(profile.identity.display_name, Some("Test User".to_string()));
|
||||
assert_eq!(profile.identity.first_name, Some("Test".to_string()));
|
||||
assert_eq!(profile.identity.last_name, Some("User".to_string()));
|
||||
assert_eq!(profile.identity.email, Some("test@example.com".to_string()));
|
||||
assert_eq!(profile.preferences.theme, Some("dark".to_string()));
|
||||
assert_eq!(profile.preferences.language, Some("en".to_string()));
|
||||
assert!(profile.preferences.notifications_enabled);
|
||||
assert!(!profile.preferences.auto_sync);
|
||||
assert!(!profile.is_default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_user_profile() {
|
||||
use crate::domain::entities::{UserIdentity, UserPreferences, UserProfile};
|
||||
|
||||
let profile = UserProfile {
|
||||
id: "test-uuid-default".to_string(),
|
||||
hub_did: None,
|
||||
identity: UserIdentity {
|
||||
handle: None,
|
||||
display_name: None,
|
||||
first_name: None,
|
||||
last_name: None,
|
||||
email: None,
|
||||
avatar_url: None,
|
||||
bio: None,
|
||||
},
|
||||
preferences: UserPreferences {
|
||||
theme: None,
|
||||
language: None,
|
||||
notifications_enabled: true,
|
||||
auto_sync: true,
|
||||
},
|
||||
created_at: 1234567890,
|
||||
updated_at: 1234567890,
|
||||
};
|
||||
|
||||
assert!(profile.is_default());
|
||||
assert_eq!(profile.hub_did, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_passport_user_profile_management() {
|
||||
use crate::domain::entities::{Passport, UserIdentity, UserPreferences, UserProfile, Seed, PublicKey, PrivateKey};
|
||||
|
||||
let seed = Seed::new(vec![1, 2, 3, 4, 5]);
|
||||
let public_key = PublicKey(vec![1, 2, 3]);
|
||||
let private_key = PrivateKey(vec![4, 5, 6]);
|
||||
let univ_id = "test-universe".to_string();
|
||||
|
||||
let mut passport = Passport::new(seed, public_key, private_key, univ_id);
|
||||
|
||||
// Test default profile exists
|
||||
assert!(passport.default_user_profile().is_some());
|
||||
assert_eq!(passport.user_profiles().len(), 1);
|
||||
|
||||
// Test adding a hub-specific profile
|
||||
let hub_profile = UserProfile {
|
||||
id: "test-uuid-hub".to_string(),
|
||||
hub_did: Some("h:example".to_string()),
|
||||
identity: UserIdentity {
|
||||
handle: Some("hubuser".to_string()),
|
||||
display_name: Some("Hub User".to_string()),
|
||||
first_name: Some("Hub".to_string()),
|
||||
last_name: Some("User".to_string()),
|
||||
email: None,
|
||||
avatar_url: None,
|
||||
bio: None,
|
||||
},
|
||||
preferences: UserPreferences {
|
||||
theme: Some("light".to_string()),
|
||||
language: None,
|
||||
notifications_enabled: false,
|
||||
auto_sync: true,
|
||||
},
|
||||
created_at: 1234567890,
|
||||
updated_at: 1234567890,
|
||||
};
|
||||
|
||||
let result = passport.add_user_profile(hub_profile);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(passport.user_profiles().len(), 2);
|
||||
|
||||
// Test finding profile by hub DID
|
||||
let found_profile = passport.user_profile_for_hub("h:example");
|
||||
assert!(found_profile.is_some());
|
||||
assert_eq!(found_profile.unwrap().identity.handle, Some("hubuser".to_string()));
|
||||
assert_eq!(found_profile.unwrap().identity.display_name, Some("Hub User".to_string()));
|
||||
|
||||
// Test duplicate hub DID rejection
|
||||
let duplicate_profile = UserProfile {
|
||||
id: "test-uuid-duplicate".to_string(),
|
||||
hub_did: Some("h:example".to_string()),
|
||||
identity: UserIdentity {
|
||||
handle: Some("anotheruser".to_string()),
|
||||
display_name: Some("Another User".to_string()),
|
||||
first_name: Some("Another".to_string()),
|
||||
last_name: Some("User".to_string()),
|
||||
email: None,
|
||||
avatar_url: None,
|
||||
bio: None,
|
||||
},
|
||||
preferences: UserPreferences {
|
||||
theme: None,
|
||||
language: None,
|
||||
notifications_enabled: true,
|
||||
auto_sync: false,
|
||||
},
|
||||
created_at: 1234567890,
|
||||
updated_at: 1234567890,
|
||||
};
|
||||
|
||||
let result = passport.add_user_profile(duplicate_profile);
|
||||
assert!(result.is_err());
|
||||
|
||||
// Test updating profile
|
||||
let hub_profile_id = passport.user_profile_for_hub("h:example").unwrap().id.clone();
|
||||
|
||||
let updated_profile = UserProfile {
|
||||
id: hub_profile_id.clone(), // Same ID as original
|
||||
hub_did: Some("h:example".to_string()),
|
||||
identity: UserIdentity {
|
||||
handle: Some("updateduser".to_string()),
|
||||
display_name: Some("Updated User".to_string()),
|
||||
first_name: Some("Updated".to_string()),
|
||||
last_name: Some("User".to_string()),
|
||||
email: None,
|
||||
avatar_url: None,
|
||||
bio: None,
|
||||
},
|
||||
preferences: UserPreferences {
|
||||
theme: Some("dark".to_string()),
|
||||
language: None,
|
||||
notifications_enabled: true,
|
||||
auto_sync: false,
|
||||
},
|
||||
created_at: 1234567890,
|
||||
updated_at: 1234567890,
|
||||
};
|
||||
|
||||
let result = passport.update_user_profile_by_id(&hub_profile_id, updated_profile);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let found_profile = passport.user_profile_for_hub("h:example");
|
||||
assert_eq!(found_profile.unwrap().identity.handle, Some("updateduser".to_string()));
|
||||
assert_eq!(found_profile.unwrap().identity.display_name, Some("Updated User".to_string()));
|
||||
|
||||
// Test removing profile
|
||||
let result = passport.remove_user_profile_by_id(&hub_profile_id);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(passport.user_profiles().len(), 1);
|
||||
|
||||
// Test cannot remove default profile
|
||||
let default_profile_id = passport.default_user_profile().unwrap().id.clone();
|
||||
let result = passport.remove_user_profile_by_id(&default_profile_id);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ pub trait KeyDeriver {
|
|||
type Error: Into<DomainError>;
|
||||
|
||||
fn derive_from_seed(&self, seed: &Seed) -> Result<(PublicKey, PrivateKey), Self::Error>;
|
||||
fn derive_from_mnemonic(&self, mnemonic: &RecoveryPhrase) -> Result<Seed, Self::Error>;
|
||||
fn derive_from_mnemonic(&self, mnemonic: &RecoveryPhrase, univ_id: &str) -> Result<Seed, Self::Error>;
|
||||
}
|
||||
|
||||
pub trait FileEncryptor {
|
||||
|
|
@ -24,13 +24,15 @@ pub trait FileEncryptor {
|
|||
password: &str,
|
||||
public_key: &PublicKey,
|
||||
did: &Did,
|
||||
univ_id: &str,
|
||||
user_profiles: &[UserProfile],
|
||||
) -> Result<PassportFile, Self::Error>;
|
||||
|
||||
fn decrypt(
|
||||
&self,
|
||||
file: &PassportFile,
|
||||
password: &str,
|
||||
) -> Result<(Seed, PublicKey, PrivateKey), Self::Error>;
|
||||
) -> Result<(Seed, PublicKey, PrivateKey, Vec<UserProfile>), Self::Error>;
|
||||
}
|
||||
|
||||
pub trait FileStorage {
|
||||
|
|
|
|||
|
|
@ -53,13 +53,13 @@ impl KeyDeriver for Ed25519KeyDeriver {
|
|||
))
|
||||
}
|
||||
|
||||
fn derive_from_mnemonic(&self, mnemonic: &RecoveryPhrase) -> Result<Seed, Self::Error> {
|
||||
fn derive_from_mnemonic(&self, mnemonic: &RecoveryPhrase, univ_id: &str) -> Result<Seed, Self::Error> {
|
||||
let phrase = mnemonic.words().join(" ");
|
||||
let bip39_mnemonic = Mnemonic::parse(&phrase)
|
||||
.map_err(|e| DomainError::InvalidMnemonic(format!("Invalid mnemonic: {}", e)))?;
|
||||
|
||||
// Use empty passphrase for now
|
||||
let bip39_seed = bip39_mnemonic.to_seed("");
|
||||
// Use univ_id as passphrase to bind seed to universe
|
||||
let bip39_seed = bip39_mnemonic.to_seed(univ_id);
|
||||
Ok(Seed::new(bip39_seed.to_vec()))
|
||||
}
|
||||
}
|
||||
|
|
@ -76,6 +76,8 @@ impl FileEncryptor for XChaCha20FileEncryptor {
|
|||
password: &str,
|
||||
public_key: &PublicKey,
|
||||
did: &Did,
|
||||
univ_id: &str,
|
||||
user_profiles: &[UserProfile],
|
||||
) -> Result<PassportFile, Self::Error> {
|
||||
// Generate salt and nonce
|
||||
let mut salt = [0u8; 32];
|
||||
|
|
@ -96,6 +98,14 @@ impl FileEncryptor for XChaCha20FileEncryptor {
|
|||
.encrypt(nonce, seed.as_bytes())
|
||||
.map_err(|e| DomainError::CryptographicError(format!("Encryption failed: {}", e)))?;
|
||||
|
||||
// Serialize and encrypt user profiles
|
||||
let user_profiles_vec: Vec<UserProfile> = user_profiles.to_vec();
|
||||
let user_profiles_bytes = serde_cbor::to_vec(&user_profiles_vec)
|
||||
.map_err(|e| DomainError::CryptographicError(format!("Failed to serialize user profiles: {}", e)))?;
|
||||
let enc_user_profiles = cipher
|
||||
.encrypt(nonce, &*user_profiles_bytes)
|
||||
.map_err(|e| DomainError::CryptographicError(format!("User profiles encryption failed: {}", e)))?;
|
||||
|
||||
// Get current timestamp
|
||||
let created_at = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
|
|
@ -110,8 +120,10 @@ impl FileEncryptor for XChaCha20FileEncryptor {
|
|||
nonce: nonce_bytes.to_vec(),
|
||||
public_key: public_key.0.clone(),
|
||||
did: did.0.clone(),
|
||||
univ_id: univ_id.to_string(),
|
||||
created_at,
|
||||
version: "1.0.0".to_string(),
|
||||
enc_user_profiles,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -119,7 +131,7 @@ impl FileEncryptor for XChaCha20FileEncryptor {
|
|||
&self,
|
||||
file: &PassportFile,
|
||||
password: &str,
|
||||
) -> Result<(Seed, PublicKey, PrivateKey), Self::Error> {
|
||||
) -> Result<(Seed, PublicKey, PrivateKey, Vec<UserProfile>), Self::Error> {
|
||||
// Validate file format
|
||||
if file.kdf != "HKDF-SHA256" || file.cipher != "XChaCha20-Poly1305" {
|
||||
return Err(DomainError::InvalidFileFormat(
|
||||
|
|
@ -153,6 +165,14 @@ impl FileEncryptor for XChaCha20FileEncryptor {
|
|||
));
|
||||
}
|
||||
|
||||
Ok((seed, public_key, private_key))
|
||||
// Decrypt user profiles
|
||||
let user_profiles_bytes = cipher
|
||||
.decrypt(nonce, &*file.enc_user_profiles)
|
||||
.map_err(|e| DomainError::CryptographicError(format!("User profiles decryption failed: {}", e)))?;
|
||||
let user_profiles: Vec<UserProfile> = serde_cbor::from_slice(&user_profiles_bytes)
|
||||
.map_err(|e| DomainError::CryptographicError(format!("Failed to deserialize user profiles: {}", 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))
|
||||
}
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ mod tests {
|
|||
let password = "test-password";
|
||||
|
||||
// Encrypt
|
||||
let encrypted_file = encryptor.encrypt(&seed, password, &public_key, &did).unwrap();
|
||||
let encrypted_file = encryptor.encrypt(&seed, password, &public_key, &did, "univ:test:crypto", &[]).unwrap();
|
||||
|
||||
// Verify file structure
|
||||
assert_eq!(encrypted_file.kdf, "HKDF-SHA256");
|
||||
|
|
@ -74,7 +74,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());
|
||||
|
|
@ -90,7 +90,7 @@ mod tests {
|
|||
let did = Did::new(&public_key);
|
||||
|
||||
// Encrypt with one password
|
||||
let encrypted_file = encryptor.encrypt(&seed, "correct-password", &public_key, &did).unwrap();
|
||||
let encrypted_file = encryptor.encrypt(&seed, "correct-password", &public_key, &did, "univ:test:crypto", &[]).unwrap();
|
||||
|
||||
// Try to decrypt with wrong password
|
||||
let result = encryptor.decrypt(&encrypted_file, "wrong-password");
|
||||
|
|
@ -111,8 +111,10 @@ mod tests {
|
|||
nonce: vec![0; 24],
|
||||
public_key: vec![1; 32],
|
||||
did: "test-did".to_string(),
|
||||
univ_id: "univ:test:crypto".to_string(),
|
||||
created_at: 0,
|
||||
version: "1.0.0".to_string(),
|
||||
enc_user_profiles: vec![],
|
||||
};
|
||||
|
||||
// Test with invalid KDF
|
||||
|
|
@ -125,4 +127,129 @@ mod tests {
|
|||
let result = encryptor.decrypt(&invalid_file, "password");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_encryptor_with_user_profiles() {
|
||||
let encryptor = XChaCha20FileEncryptor;
|
||||
let key_deriver = Ed25519KeyDeriver;
|
||||
|
||||
let seed = Seed::new(vec![1; 32]);
|
||||
let (public_key, _) = key_deriver.derive_from_seed(&seed).unwrap();
|
||||
let did = Did::new(&public_key);
|
||||
let password = "test-password";
|
||||
|
||||
// Create test user profiles
|
||||
let user_profiles = vec![
|
||||
UserProfile {
|
||||
id: "test-uuid-default".to_string(),
|
||||
hub_did: None,
|
||||
identity: UserIdentity {
|
||||
handle: Some("defaultuser".to_string()),
|
||||
display_name: Some("Default User".to_string()),
|
||||
first_name: Some("Default".to_string()),
|
||||
last_name: Some("User".to_string()),
|
||||
email: Some("default@example.com".to_string()),
|
||||
avatar_url: None,
|
||||
bio: Some("Default bio".to_string()),
|
||||
},
|
||||
preferences: UserPreferences {
|
||||
theme: Some("dark".to_string()),
|
||||
language: Some("en".to_string()),
|
||||
notifications_enabled: true,
|
||||
auto_sync: true,
|
||||
},
|
||||
created_at: 1234567890,
|
||||
updated_at: 1234567890,
|
||||
},
|
||||
UserProfile {
|
||||
id: "test-uuid-hub".to_string(),
|
||||
hub_did: Some("h:hub1".to_string()),
|
||||
identity: UserIdentity {
|
||||
handle: Some("hubuser".to_string()),
|
||||
display_name: Some("Hub User".to_string()),
|
||||
first_name: Some("Hub".to_string()),
|
||||
last_name: Some("User".to_string()),
|
||||
email: Some("hub@example.com".to_string()),
|
||||
avatar_url: Some("https://example.com/avatar.png".to_string()),
|
||||
bio: Some("Hub bio".to_string()),
|
||||
},
|
||||
preferences: UserPreferences {
|
||||
theme: Some("light".to_string()),
|
||||
language: Some("fr".to_string()),
|
||||
notifications_enabled: false,
|
||||
auto_sync: false,
|
||||
},
|
||||
created_at: 1234567891,
|
||||
updated_at: 1234567892,
|
||||
},
|
||||
];
|
||||
|
||||
// Encrypt
|
||||
let encrypted_file = encryptor.encrypt(
|
||||
&seed,
|
||||
password,
|
||||
&public_key,
|
||||
&did,
|
||||
"univ:test:crypto",
|
||||
&user_profiles
|
||||
).unwrap();
|
||||
|
||||
// Verify file structure includes user profiles
|
||||
assert_eq!(encrypted_file.kdf, "HKDF-SHA256");
|
||||
assert_eq!(encrypted_file.cipher, "XChaCha20-Poly1305");
|
||||
assert!(!encrypted_file.enc_user_profiles.is_empty());
|
||||
|
||||
// Decrypt
|
||||
let (decrypted_seed, decrypted_public_key, _, decrypted_profiles) =
|
||||
encryptor.decrypt(&encrypted_file, password).unwrap();
|
||||
|
||||
// Verify decryption
|
||||
assert_eq!(decrypted_seed.as_bytes(), seed.as_bytes());
|
||||
assert_eq!(decrypted_public_key.0, public_key.0);
|
||||
|
||||
// Verify user profiles
|
||||
assert_eq!(decrypted_profiles.len(), 2);
|
||||
|
||||
// Verify default profile
|
||||
let default_profile = decrypted_profiles.iter().find(|p| p.is_default()).unwrap();
|
||||
assert_eq!(default_profile.identity.handle, Some("defaultuser".to_string()));
|
||||
assert_eq!(default_profile.identity.display_name, Some("Default User".to_string()));
|
||||
assert_eq!(default_profile.identity.email, Some("default@example.com".to_string()));
|
||||
assert_eq!(default_profile.preferences.theme, Some("dark".to_string()));
|
||||
|
||||
// Verify hub profile
|
||||
let hub_profile = decrypted_profiles.iter().find(|p| p.hub_did == Some("h:hub1".to_string())).unwrap();
|
||||
assert_eq!(hub_profile.identity.handle, Some("hubuser".to_string()));
|
||||
assert_eq!(hub_profile.identity.display_name, Some("Hub User".to_string()));
|
||||
assert_eq!(hub_profile.identity.email, Some("hub@example.com".to_string()));
|
||||
assert_eq!(hub_profile.preferences.language, Some("fr".to_string()));
|
||||
assert!(!hub_profile.preferences.notifications_enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_encryptor_with_empty_user_profiles() {
|
||||
let encryptor = XChaCha20FileEncryptor;
|
||||
let key_deriver = Ed25519KeyDeriver;
|
||||
|
||||
let seed = Seed::new(vec![1; 32]);
|
||||
let (public_key, _) = key_deriver.derive_from_seed(&seed).unwrap();
|
||||
let did = Did::new(&public_key);
|
||||
let password = "test-password";
|
||||
|
||||
// Encrypt with empty user profiles
|
||||
let encrypted_file = encryptor.encrypt(
|
||||
&seed,
|
||||
password,
|
||||
&public_key,
|
||||
&did,
|
||||
"univ:test:crypto",
|
||||
&[]
|
||||
).unwrap();
|
||||
|
||||
// Decrypt
|
||||
let (_, _, _, decrypted_profiles) = encryptor.decrypt(&encrypted_file, password).unwrap();
|
||||
|
||||
// Should have empty profiles
|
||||
assert_eq!(decrypted_profiles.len(), 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,8 +18,10 @@ mod tests {
|
|||
nonce: vec![0; 24],
|
||||
public_key: vec![1; 32],
|
||||
did: "test-did".to_string(),
|
||||
univ_id: "univ:test:storage".to_string(),
|
||||
created_at: 1234567890,
|
||||
version: "1.0.0".to_string(),
|
||||
enc_user_profiles: vec![],
|
||||
};
|
||||
|
||||
// Save file
|
||||
|
|
|
|||
232
libs/sharenet-passport/src/universe_binding_test.rs
Normal file
232
libs/sharenet-passport/src/universe_binding_test.rs
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
use crate::application::use_cases::*;
|
||||
use crate::domain::entities::*;
|
||||
use crate::infrastructure::crypto::*;
|
||||
use crate::infrastructure::storage::*;
|
||||
use std::fs;
|
||||
|
||||
#[cfg(test)]
|
||||
mod universe_binding_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_passport_creation_with_different_universes() {
|
||||
let mnemonic_generator = Bip39MnemonicGenerator;
|
||||
let key_deriver = Ed25519KeyDeriver;
|
||||
let file_encryptor = XChaCha20FileEncryptor;
|
||||
let file_storage = FileSystemStorage;
|
||||
|
||||
let create_use_case = CreatePassportUseCase::new(
|
||||
mnemonic_generator.clone(),
|
||||
key_deriver.clone(),
|
||||
file_encryptor.clone(),
|
||||
file_storage.clone(),
|
||||
);
|
||||
|
||||
// Create passports for different universes with the same mnemonic
|
||||
let univ1 = "univ:test:alpha";
|
||||
let univ2 = "univ:test:beta";
|
||||
let password = "test_password";
|
||||
|
||||
// Create first passport
|
||||
let (passport1, recovery_phrase) = create_use_case
|
||||
.execute(univ1, password, "/tmp/test_passport1.spf")
|
||||
.expect("Failed to create passport 1");
|
||||
|
||||
// Create second passport with same mnemonic but different universe
|
||||
let import_use_case = ImportFromRecoveryUseCase::new(
|
||||
mnemonic_generator,
|
||||
key_deriver,
|
||||
file_encryptor,
|
||||
file_storage,
|
||||
);
|
||||
|
||||
let passport2 = import_use_case
|
||||
.execute(
|
||||
univ2,
|
||||
&recovery_phrase.words(),
|
||||
password,
|
||||
"/tmp/test_passport2.spf",
|
||||
)
|
||||
.expect("Failed to create passport 2");
|
||||
|
||||
// Verify universe binding
|
||||
assert_eq!(passport1.univ_id(), univ1);
|
||||
assert_eq!(passport2.univ_id(), univ2);
|
||||
|
||||
// Verify DIDs are universe-bound
|
||||
assert!(passport1.did().as_str().contains(univ1));
|
||||
assert!(passport2.did().as_str().contains(univ2));
|
||||
assert_ne!(passport1.did().as_str(), passport2.did().as_str());
|
||||
|
||||
// Verify public keys are different (due to universe binding)
|
||||
assert_ne!(
|
||||
hex::encode(&passport1.public_key().0),
|
||||
hex::encode(&passport2.public_key().0)
|
||||
);
|
||||
|
||||
// Clean up
|
||||
let _ = fs::remove_file("/tmp/test_passport1.spf");
|
||||
let _ = fs::remove_file("/tmp/test_passport2.spf");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_universe_bound_card_signing() {
|
||||
let mnemonic_generator = Bip39MnemonicGenerator;
|
||||
let key_deriver = Ed25519KeyDeriver;
|
||||
let file_encryptor = XChaCha20FileEncryptor;
|
||||
let file_storage = FileSystemStorage;
|
||||
|
||||
let create_use_case = CreatePassportUseCase::new(
|
||||
mnemonic_generator,
|
||||
key_deriver,
|
||||
file_encryptor,
|
||||
file_storage,
|
||||
);
|
||||
|
||||
let univ_id = "univ:test:signing";
|
||||
let password = "test_password";
|
||||
|
||||
let (passport, _) = create_use_case
|
||||
.execute(univ_id, password, "/tmp/test_signing.spf")
|
||||
.expect("Failed to create passport");
|
||||
|
||||
let sign_use_case = SignCardUseCase::new();
|
||||
let message = "Hello, universe!";
|
||||
let signature = sign_use_case
|
||||
.execute(&passport, message)
|
||||
.expect("Failed to sign message");
|
||||
|
||||
// Verify signature is universe-bound
|
||||
let signing_key = ed25519_dalek::SigningKey::from_bytes(
|
||||
&passport.private_key.0[..32].try_into().unwrap()
|
||||
);
|
||||
let verifying_key = signing_key.verifying_key();
|
||||
|
||||
// Correct universe-bound message should verify
|
||||
let correct_message = format!("univ:{}:{}", univ_id, message);
|
||||
assert!(verifying_key
|
||||
.verify_strict(correct_message.as_bytes(), &ed25519_dalek::Signature::from_bytes(&signature).unwrap())
|
||||
.is_ok());
|
||||
|
||||
// Wrong universe message should NOT verify
|
||||
let wrong_message = format!("univ:{}:{}", "univ:wrong:universe", message);
|
||||
assert!(verifying_key
|
||||
.verify_strict(wrong_message.as_bytes(), &ed25519_dalek::Signature::from_bytes(&signature).unwrap())
|
||||
.is_err());
|
||||
|
||||
// Clean up
|
||||
let _ = fs::remove_file("/tmp/test_signing.spf");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_passport_file_stores_univ_id() {
|
||||
let mnemonic_generator = Bip39MnemonicGenerator;
|
||||
let key_deriver = Ed25519KeyDeriver;
|
||||
let file_encryptor = XChaCha20FileEncryptor;
|
||||
let file_storage = FileSystemStorage;
|
||||
|
||||
let create_use_case = CreatePassportUseCase::new(
|
||||
mnemonic_generator,
|
||||
key_deriver,
|
||||
file_encryptor.clone(),
|
||||
file_storage.clone(),
|
||||
);
|
||||
|
||||
let univ_id = "univ:test:storage";
|
||||
let password = "test_password";
|
||||
|
||||
let (passport, _) = create_use_case
|
||||
.execute(univ_id, password, "/tmp/test_storage.spf")
|
||||
.expect("Failed to create passport");
|
||||
|
||||
// Load the file and verify univ_id is stored
|
||||
let loaded_file = file_storage
|
||||
.load("/tmp/test_storage.spf")
|
||||
.expect("Failed to load passport file");
|
||||
|
||||
assert_eq!(loaded_file.univ_id, univ_id);
|
||||
|
||||
// Import from file and verify univ_id is preserved
|
||||
let import_use_case = ImportFromFileUseCase::new(file_encryptor, file_storage);
|
||||
let imported_passport = import_use_case
|
||||
.execute("/tmp/test_storage.spf", password, None)
|
||||
.expect("Failed to import passport");
|
||||
|
||||
assert_eq!(imported_passport.univ_id(), univ_id);
|
||||
assert_eq!(imported_passport.did().as_str(), passport.did().as_str());
|
||||
|
||||
// Clean up
|
||||
let _ = fs::remove_file("/tmp/test_storage.spf");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cross_universe_prevention() {
|
||||
let mnemonic_generator = Bip39MnemonicGenerator;
|
||||
let key_deriver = Ed25519KeyDeriver;
|
||||
let file_encryptor = XChaCha20FileEncryptor;
|
||||
let file_storage = FileSystemStorage;
|
||||
|
||||
let create_use_case = CreatePassportUseCase::new(
|
||||
mnemonic_generator.clone(),
|
||||
key_deriver.clone(),
|
||||
file_encryptor.clone(),
|
||||
file_storage.clone(),
|
||||
);
|
||||
|
||||
let univ1 = "univ:test:security1";
|
||||
let univ2 = "univ:test:security2";
|
||||
let password = "test_password";
|
||||
|
||||
// Create passport for universe 1
|
||||
let (passport1, recovery_phrase) = create_use_case
|
||||
.execute(univ1, password, "/tmp/test_security1.spf")
|
||||
.expect("Failed to create passport 1");
|
||||
|
||||
// Try to import same mnemonic into universe 2
|
||||
let import_use_case = ImportFromRecoveryUseCase::new(
|
||||
mnemonic_generator,
|
||||
key_deriver,
|
||||
file_encryptor,
|
||||
file_storage,
|
||||
);
|
||||
|
||||
let passport2 = import_use_case
|
||||
.execute(
|
||||
univ2,
|
||||
&recovery_phrase.words(),
|
||||
password,
|
||||
"/tmp/test_security2.spf",
|
||||
)
|
||||
.expect("Failed to create passport 2");
|
||||
|
||||
// Verify they are completely different identities
|
||||
assert_ne!(passport1.univ_id(), passport2.univ_id());
|
||||
assert_ne!(passport1.did().as_str(), passport2.did().as_str());
|
||||
assert_ne!(
|
||||
hex::encode(&passport1.public_key().0),
|
||||
hex::encode(&passport2.public_key().0)
|
||||
);
|
||||
|
||||
// Cards signed by passport1 should not be verifiable by passport2 and vice versa
|
||||
let sign_use_case = SignCardUseCase::new();
|
||||
let message = "Cross-universe test";
|
||||
let signature1 = sign_use_case
|
||||
.execute(&passport1, message)
|
||||
.expect("Failed to sign with passport1");
|
||||
|
||||
// Verify signature1 cannot be verified with passport2's public key
|
||||
let signing_key2 = ed25519_dalek::SigningKey::from_bytes(
|
||||
&passport2.private_key.0[..32].try_into().unwrap()
|
||||
);
|
||||
let verifying_key2 = signing_key2.verifying_key();
|
||||
|
||||
let message_for_univ1 = format!("univ:{}:{}", univ1, message);
|
||||
assert!(verifying_key2
|
||||
.verify_strict(message_for_univ1.as_bytes(), &ed25519_dalek::Signature::from_bytes(&signature1).unwrap())
|
||||
.is_err());
|
||||
|
||||
// Clean up
|
||||
let _ = fs::remove_file("/tmp/test_security1.spf");
|
||||
let _ = fs::remove_file("/tmp/test_security2.spf");
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ sharenet-passport = { path = "../libs/sharenet-passport" }
|
|||
clap = { version = "4.4", features = ["derive"] }
|
||||
rpassword = "7.2"
|
||||
hex = "0.4"
|
||||
uuid = { version = "1.7", features = ["v7"] }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.5"
|
||||
|
|
|
|||
105
sharenet-passport-cli/src/bin/test_universe_binding.rs
Normal file
105
sharenet-passport-cli/src/bin/test_universe_binding.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
use sharenet_passport::application::use_cases::*;
|
||||
use sharenet_passport::domain::traits::*;
|
||||
use sharenet_passport::infrastructure::crypto::*;
|
||||
use sharenet_passport::infrastructure::storage::*;
|
||||
use std::fs;
|
||||
|
||||
fn main() {
|
||||
println!("Testing universe binding implementation...");
|
||||
|
||||
let mnemonic_generator = Bip39MnemonicGenerator;
|
||||
let key_deriver = Ed25519KeyDeriver;
|
||||
let file_encryptor = XChaCha20FileEncryptor;
|
||||
let file_storage = FileSystemStorage;
|
||||
|
||||
let create_use_case = CreatePassportUseCase::new(
|
||||
mnemonic_generator.clone(),
|
||||
key_deriver.clone(),
|
||||
file_encryptor.clone(),
|
||||
file_storage.clone(),
|
||||
);
|
||||
|
||||
// Test 1: Create passports for different universes
|
||||
println!("\nTest 1: Creating passports for different universes...");
|
||||
let univ1 = "univ:test:alpha";
|
||||
let univ2 = "univ:test:beta";
|
||||
let password = "test_password";
|
||||
|
||||
let (passport1, recovery_phrase) = create_use_case
|
||||
.execute(univ1, password, "/tmp/test_passport1.spf")
|
||||
.expect("Failed to create passport 1");
|
||||
|
||||
println!("✓ Passport 1 created for universe: {}", passport1.univ_id());
|
||||
println!(" DID: {}", passport1.did().as_str());
|
||||
|
||||
let import_use_case = ImportFromRecoveryUseCase::new(
|
||||
mnemonic_generator,
|
||||
key_deriver,
|
||||
file_encryptor,
|
||||
file_storage,
|
||||
);
|
||||
|
||||
let passport2 = import_use_case
|
||||
.execute(
|
||||
univ2,
|
||||
&recovery_phrase.words(),
|
||||
password,
|
||||
"/tmp/test_passport2.spf",
|
||||
)
|
||||
.expect("Failed to create passport 2");
|
||||
|
||||
println!("✓ Passport 2 created for universe: {}", passport2.univ_id());
|
||||
println!(" DID: {}", passport2.did().as_str());
|
||||
|
||||
// Verify universe binding
|
||||
assert_eq!(passport1.univ_id(), univ1);
|
||||
assert_eq!(passport2.univ_id(), univ2);
|
||||
assert_ne!(passport1.did().as_str(), passport2.did().as_str());
|
||||
assert_ne!(
|
||||
hex::encode(&passport1.public_key().0),
|
||||
hex::encode(&passport2.public_key().0)
|
||||
);
|
||||
|
||||
println!("✓ Universe binding verified - different universes produce different identities");
|
||||
|
||||
// Test 2: Universe-bound card signing
|
||||
println!("\nTest 2: Testing universe-bound card signing...");
|
||||
let sign_use_case = SignCardUseCase::new();
|
||||
let message = "Hello, universe!";
|
||||
let _signature = sign_use_case
|
||||
.execute(&passport1, message)
|
||||
.expect("Failed to sign message");
|
||||
|
||||
println!("✓ Message signed successfully");
|
||||
|
||||
// Test 3: Verify PassportFile stores univ_id
|
||||
println!("\nTest 3: Verifying PassportFile stores univ_id...");
|
||||
let loaded_file = FileSystemStorage
|
||||
.load("/tmp/test_passport1.spf")
|
||||
.expect("Failed to load passport file");
|
||||
|
||||
assert_eq!(loaded_file.univ_id, univ1);
|
||||
println!("✓ PassportFile correctly stores univ_id: {}", loaded_file.univ_id);
|
||||
|
||||
// Test 4: Import from file preserves univ_id
|
||||
println!("\nTest 4: Testing import from file preserves univ_id...");
|
||||
let import_file_use_case = ImportFromFileUseCase::new(XChaCha20FileEncryptor, FileSystemStorage);
|
||||
let imported_passport = import_file_use_case
|
||||
.execute("/tmp/test_passport1.spf", password, None)
|
||||
.expect("Failed to import passport");
|
||||
|
||||
assert_eq!(imported_passport.univ_id(), univ1);
|
||||
println!("✓ Imported passport preserves univ_id: {}", imported_passport.univ_id());
|
||||
|
||||
// Clean up
|
||||
let _ = fs::remove_file("/tmp/test_passport1.spf");
|
||||
let _ = fs::remove_file("/tmp/test_passport2.spf");
|
||||
|
||||
println!("\n🎉 All universe binding tests passed successfully!");
|
||||
println!("\nSummary:");
|
||||
println!("- Passports are cryptographically bound to their universe");
|
||||
println!("- Same mnemonic + different universe = different identities");
|
||||
println!("- DIDs include universe identifier");
|
||||
println!("- Card signatures are universe-bound");
|
||||
println!("- Passport files store univ_id for verification");
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "sharenet-passport")]
|
||||
#[command(name = "sharenet-passport-cli")]
|
||||
#[command(about = "Sharenet Passport Creator - Generate and manage cryptographic identities")]
|
||||
#[command(version)]
|
||||
pub struct Cli {
|
||||
|
|
@ -13,13 +13,27 @@ pub struct Cli {
|
|||
pub enum Commands {
|
||||
/// Create a new Passport
|
||||
Create {
|
||||
/// Universe identifier (e.g., "u:My Universe:uuid")
|
||||
#[arg(short, long)]
|
||||
universe: String,
|
||||
|
||||
/// Output file path for the .spf file
|
||||
#[arg(short, long, default_value = "passport.spf")]
|
||||
output: String,
|
||||
},
|
||||
|
||||
/// Create a new universe identifier
|
||||
CreateUniverse {
|
||||
/// Universe name
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// Import a Passport from recovery phrase
|
||||
ImportRecovery {
|
||||
/// Universe identifier (e.g., "u:My Universe:uuid")
|
||||
#[arg(short, long)]
|
||||
universe: String,
|
||||
|
||||
/// Output file path for the .spf file
|
||||
#[arg(short, long, default_value = "passport.spf")]
|
||||
output: String,
|
||||
|
|
@ -59,4 +73,141 @@ pub enum Commands {
|
|||
/// Message to sign
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// User Profile Management
|
||||
Profile {
|
||||
#[command(subcommand)]
|
||||
command: ProfileCommands,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum ProfileCommands {
|
||||
/// List all user profiles
|
||||
List {
|
||||
/// .spf file path
|
||||
file: String,
|
||||
},
|
||||
|
||||
/// Create a new user profile
|
||||
Create {
|
||||
/// .spf file path
|
||||
file: String,
|
||||
|
||||
/// Hub DID (optional, omit for default profile)
|
||||
#[arg(short, long)]
|
||||
hub_did: Option<String>,
|
||||
|
||||
/// Handle
|
||||
#[arg(long)]
|
||||
handle: Option<String>,
|
||||
|
||||
/// Display name
|
||||
#[arg(short, long)]
|
||||
display_name: Option<String>,
|
||||
|
||||
/// First name
|
||||
#[arg(long)]
|
||||
first_name: Option<String>,
|
||||
|
||||
/// Last name
|
||||
#[arg(long)]
|
||||
last_name: Option<String>,
|
||||
|
||||
/// Email
|
||||
#[arg(short, long)]
|
||||
email: Option<String>,
|
||||
|
||||
/// Avatar URL
|
||||
#[arg(short, long)]
|
||||
avatar_url: Option<String>,
|
||||
|
||||
/// Bio
|
||||
#[arg(short, long)]
|
||||
bio: Option<String>,
|
||||
|
||||
/// Theme preference
|
||||
#[arg(long)]
|
||||
theme: Option<String>,
|
||||
|
||||
/// Language preference
|
||||
#[arg(long)]
|
||||
language: Option<String>,
|
||||
|
||||
/// Enable notifications
|
||||
#[arg(long)]
|
||||
notifications: bool,
|
||||
|
||||
/// Enable auto-sync
|
||||
#[arg(long)]
|
||||
auto_sync: bool,
|
||||
},
|
||||
|
||||
/// Update an existing user profile
|
||||
Update {
|
||||
/// .spf file path
|
||||
file: String,
|
||||
|
||||
/// Profile ID (required, use 'list' command to see available IDs)
|
||||
#[arg(short, long)]
|
||||
id: String,
|
||||
|
||||
/// Hub DID (optional, can be updated)
|
||||
#[arg(long)]
|
||||
hub_did: Option<String>,
|
||||
|
||||
/// Handle
|
||||
#[arg(long)]
|
||||
handle: Option<String>,
|
||||
|
||||
/// Display name
|
||||
#[arg(short, long)]
|
||||
display_name: Option<String>,
|
||||
|
||||
/// First name
|
||||
#[arg(long)]
|
||||
first_name: Option<String>,
|
||||
|
||||
/// Last name
|
||||
#[arg(long)]
|
||||
last_name: Option<String>,
|
||||
|
||||
/// Email
|
||||
#[arg(short, long)]
|
||||
email: Option<String>,
|
||||
|
||||
/// Avatar URL
|
||||
#[arg(short, long)]
|
||||
avatar_url: Option<String>,
|
||||
|
||||
/// Bio
|
||||
#[arg(short, long)]
|
||||
bio: Option<String>,
|
||||
|
||||
/// Theme preference
|
||||
#[arg(long)]
|
||||
theme: Option<String>,
|
||||
|
||||
/// Language preference
|
||||
#[arg(long)]
|
||||
language: Option<String>,
|
||||
|
||||
/// Enable notifications
|
||||
#[arg(long)]
|
||||
notifications: Option<bool>,
|
||||
|
||||
/// Enable auto-sync
|
||||
#[arg(long)]
|
||||
auto_sync: Option<bool>,
|
||||
},
|
||||
|
||||
/// Delete a user profile
|
||||
Delete {
|
||||
/// .spf file path
|
||||
file: String,
|
||||
|
||||
/// Profile ID (required, use 'list' command to see available IDs)
|
||||
#[arg(short, long)]
|
||||
id: String,
|
||||
},
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
use sharenet_passport::{
|
||||
application::use_cases::*,
|
||||
domain::entities::{UserIdentity, UserPreferences},
|
||||
infrastructure::*,
|
||||
ApplicationError,
|
||||
FileStorage,
|
||||
};
|
||||
use rpassword::prompt_password;
|
||||
use hex;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct CliInterface;
|
||||
|
||||
|
|
@ -14,9 +16,24 @@ impl CliInterface {
|
|||
Self
|
||||
}
|
||||
|
||||
pub fn handle_create(&self, output: &str) -> Result<(), ApplicationError> {
|
||||
let password = prompt_password("Enter password for new passport: ").unwrap();
|
||||
let confirm_password = prompt_password("Confirm password: ").unwrap();
|
||||
pub fn handle_create_universe(&self, name: &str) -> Result<(), ApplicationError> {
|
||||
let uuid = Uuid::now_v7();
|
||||
let universe_id = format!("u:{}:{}", name, uuid);
|
||||
|
||||
println!("🌌 Universe created successfully!");
|
||||
println!("📝 Universe Name: {}", name);
|
||||
println!("🆔 Universe ID: {}", universe_id);
|
||||
println!("\n💡 Use this Universe ID when creating Passports:");
|
||||
println!(" sharenet-passport create --universe '{}'", universe_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_create(&self, universe: &str, output: &str) -> Result<(), ApplicationError> {
|
||||
let password = prompt_password("Enter password for new passport: ")
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
|
||||
let confirm_password = prompt_password("Confirm password: ")
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
|
||||
|
||||
if password != confirm_password {
|
||||
return Err(ApplicationError::UseCaseError("Passwords do not match".to_string()));
|
||||
|
|
@ -29,7 +46,7 @@ impl CliInterface {
|
|||
FileSystemStorage,
|
||||
);
|
||||
|
||||
let (passport, recovery_phrase) = use_case.execute(&password, output)?;
|
||||
let (passport, recovery_phrase) = use_case.execute(universe, &password, output)?;
|
||||
|
||||
println!("✅ Passport created successfully!");
|
||||
println!("📄 Saved to: {}", output);
|
||||
|
|
@ -41,17 +58,20 @@ impl CliInterface {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_import_recovery(&self, output: &str) -> Result<(), ApplicationError> {
|
||||
pub fn handle_import_recovery(&self, universe: &str, output: &str) -> Result<(), ApplicationError> {
|
||||
println!("Enter your 24-word recovery phrase:");
|
||||
let mut recovery_words = Vec::new();
|
||||
|
||||
for i in 1..=24 {
|
||||
let word = prompt_password(&format!("Word {}: ", i)).unwrap();
|
||||
let word = prompt_password(&format!("Word {}: ", i))
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read recovery word: {}", e)))?;
|
||||
recovery_words.push(word);
|
||||
}
|
||||
|
||||
let password = prompt_password("Enter new password for passport file: ").unwrap();
|
||||
let confirm_password = prompt_password("Confirm password: ").unwrap();
|
||||
let password = prompt_password("Enter new password for passport file: ")
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
|
||||
let confirm_password = prompt_password("Confirm password: ")
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
|
||||
|
||||
if password != confirm_password {
|
||||
return Err(ApplicationError::UseCaseError("Passwords do not match".to_string()));
|
||||
|
|
@ -64,7 +84,7 @@ impl CliInterface {
|
|||
FileSystemStorage,
|
||||
);
|
||||
|
||||
let passport = use_case.execute(&recovery_words, &password, output)?;
|
||||
let passport = use_case.execute(universe, &recovery_words, &password, output)?;
|
||||
|
||||
println!("✅ Passport imported successfully!");
|
||||
println!("📄 Saved to: {}", output);
|
||||
|
|
@ -75,7 +95,8 @@ impl CliInterface {
|
|||
}
|
||||
|
||||
pub fn handle_import_file(&self, input: &str, output: Option<&str>) -> Result<(), ApplicationError> {
|
||||
let password = prompt_password("Enter password for passport file: ").unwrap();
|
||||
let password = prompt_password("Enter password for passport file: ")
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
|
||||
|
||||
let use_case = ImportFromFileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
|
|
@ -95,9 +116,12 @@ impl CliInterface {
|
|||
}
|
||||
|
||||
pub fn handle_export(&self, input: &str, output: &str) -> Result<(), ApplicationError> {
|
||||
let password = prompt_password("Enter password for passport file: ").unwrap();
|
||||
let new_password = prompt_password("Enter new password for exported file: ").unwrap();
|
||||
let confirm_password = prompt_password("Confirm new password: ").unwrap();
|
||||
let password = prompt_password("Enter password for passport file: ")
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
|
||||
let new_password = prompt_password("Enter new password for exported file: ")
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
|
||||
let confirm_password = prompt_password("Confirm new password: ")
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
|
||||
|
||||
if new_password != confirm_password {
|
||||
return Err(ApplicationError::UseCaseError("Passwords do not match".to_string()));
|
||||
|
|
@ -131,6 +155,7 @@ impl CliInterface {
|
|||
|
||||
println!("📄 Passport File Information:");
|
||||
println!(" File: {}", file);
|
||||
println!(" Universe ID: {}", passport_file.univ_id);
|
||||
println!(" Version: {}", passport_file.version);
|
||||
println!(" Created: {}", passport_file.created_at);
|
||||
println!(" DID: {}", passport_file.did);
|
||||
|
|
@ -142,7 +167,8 @@ impl CliInterface {
|
|||
}
|
||||
|
||||
pub fn handle_sign(&self, file: &str, message: &str) -> Result<(), ApplicationError> {
|
||||
let password = prompt_password("Enter password for passport file: ").unwrap();
|
||||
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,
|
||||
|
|
@ -160,4 +186,252 @@ impl CliInterface {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_profile_list(&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!("👤 User Profiles:");
|
||||
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" });
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_profile_create(
|
||||
&self,
|
||||
file: &str,
|
||||
hub_did: Option<String>,
|
||||
handle: Option<String>,
|
||||
display_name: Option<String>,
|
||||
first_name: Option<String>,
|
||||
last_name: Option<String>,
|
||||
email: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
bio: Option<String>,
|
||||
theme: Option<String>,
|
||||
language: Option<String>,
|
||||
notifications: bool,
|
||||
auto_sync: 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 identity = UserIdentity {
|
||||
handle,
|
||||
display_name,
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
avatar_url,
|
||||
bio,
|
||||
};
|
||||
|
||||
let preferences = UserPreferences {
|
||||
theme,
|
||||
language,
|
||||
notifications_enabled: notifications,
|
||||
auto_sync,
|
||||
};
|
||||
|
||||
let create_use_case = CreateUserProfileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
create_use_case.execute(
|
||||
&mut passport,
|
||||
hub_did.clone(),
|
||||
identity,
|
||||
preferences,
|
||||
&password,
|
||||
file,
|
||||
)?;
|
||||
|
||||
println!("✅ User profile created successfully!");
|
||||
if let Some(hub_did) = hub_did {
|
||||
println!("📡 Hub DID: {}", hub_did);
|
||||
} else {
|
||||
println!("🏠 Profile Type: Default");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_profile_update(
|
||||
&self,
|
||||
file: &str,
|
||||
id: &str,
|
||||
hub_did: Option<String>,
|
||||
handle: Option<String>,
|
||||
display_name: Option<String>,
|
||||
first_name: Option<String>,
|
||||
last_name: Option<String>,
|
||||
email: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
bio: Option<String>,
|
||||
theme: Option<String>,
|
||||
language: Option<String>,
|
||||
notifications: Option<bool>,
|
||||
auto_sync: Option<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)?;
|
||||
|
||||
// Get existing profile by ID
|
||||
let existing_profile = passport.user_profile_by_id(id)
|
||||
.ok_or_else(|| ApplicationError::UseCaseError("User profile not found".to_string()))?;
|
||||
|
||||
let identity = UserIdentity {
|
||||
handle: handle.or_else(|| existing_profile.identity.handle.clone()),
|
||||
display_name: display_name.or_else(|| existing_profile.identity.display_name.clone()),
|
||||
first_name: first_name.or_else(|| existing_profile.identity.first_name.clone()),
|
||||
last_name: last_name.or_else(|| existing_profile.identity.last_name.clone()),
|
||||
email: email.or_else(|| existing_profile.identity.email.clone()),
|
||||
avatar_url: avatar_url.or_else(|| existing_profile.identity.avatar_url.clone()),
|
||||
bio: bio.or_else(|| existing_profile.identity.bio.clone()),
|
||||
};
|
||||
|
||||
let preferences = UserPreferences {
|
||||
theme: theme.or_else(|| existing_profile.preferences.theme.clone()),
|
||||
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),
|
||||
};
|
||||
|
||||
// Clone values before using them in multiple places
|
||||
let identity_clone = identity.clone();
|
||||
let preferences_clone = preferences.clone();
|
||||
let hub_did_clone = hub_did.clone();
|
||||
|
||||
// Create updated profile with new hub_did if provided
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_err(|e| ApplicationError::UseCaseError(format!("Time error: {}", e)))?
|
||||
.as_secs();
|
||||
|
||||
let _profile = sharenet_passport::domain::entities::UserProfile {
|
||||
id: existing_profile.id.clone(),
|
||||
hub_did: hub_did.or_else(|| existing_profile.hub_did.clone()),
|
||||
identity,
|
||||
preferences,
|
||||
created_at: existing_profile.created_at,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
// Use the update use case to handle the profile update and file saving
|
||||
let update_use_case = UpdateUserProfileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
update_use_case.execute(
|
||||
&mut passport,
|
||||
Some(id),
|
||||
identity_clone,
|
||||
preferences_clone,
|
||||
&password,
|
||||
file,
|
||||
)?;
|
||||
|
||||
println!("✅ User profile updated successfully!");
|
||||
if let Some(hub_did) = hub_did_clone {
|
||||
println!("📡 Hub DID: {}", hub_did);
|
||||
} else {
|
||||
println!("🏠 Profile Type: Default");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_profile_delete(&self, file: &str, id: &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 mut passport = import_use_case.execute(file, &password, None)?;
|
||||
|
||||
// Use the delete use case to handle the profile removal and file saving
|
||||
let delete_use_case = DeleteUserProfileUseCase::new(
|
||||
XChaCha20FileEncryptor,
|
||||
FileSystemStorage,
|
||||
);
|
||||
|
||||
delete_use_case.execute(
|
||||
&mut passport,
|
||||
Some(id),
|
||||
&password,
|
||||
file,
|
||||
)?;
|
||||
|
||||
println!("✅ User profile deleted successfully!");
|
||||
println!("🆔 Profile ID: {}", id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -10,11 +10,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
let interface = CliInterface::new();
|
||||
|
||||
match cli.command {
|
||||
Commands::Create { output } => {
|
||||
interface.handle_create(&output)?;
|
||||
Commands::Create { universe, output } => {
|
||||
interface.handle_create(&universe, &output)?;
|
||||
}
|
||||
Commands::ImportRecovery { output } => {
|
||||
interface.handle_import_recovery(&output)?;
|
||||
Commands::CreateUniverse { name } => {
|
||||
interface.handle_create_universe(&name)?;
|
||||
}
|
||||
Commands::ImportRecovery { universe, output } => {
|
||||
interface.handle_import_recovery(&universe, &output)?;
|
||||
}
|
||||
Commands::ImportFile { input, output } => {
|
||||
interface.handle_import_file(&input, output.as_deref())?;
|
||||
|
|
@ -28,6 +31,80 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
Commands::Sign { file, message } => {
|
||||
interface.handle_sign(&file, &message)?;
|
||||
}
|
||||
Commands::Profile { command } => {
|
||||
match command {
|
||||
crate::cli::commands::ProfileCommands::List { file } => {
|
||||
interface.handle_profile_list(&file)?;
|
||||
}
|
||||
crate::cli::commands::ProfileCommands::Create {
|
||||
file,
|
||||
hub_did,
|
||||
handle,
|
||||
display_name,
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
avatar_url,
|
||||
bio,
|
||||
theme,
|
||||
language,
|
||||
notifications,
|
||||
auto_sync,
|
||||
} => {
|
||||
interface.handle_profile_create(
|
||||
&file,
|
||||
hub_did,
|
||||
handle,
|
||||
display_name,
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
avatar_url,
|
||||
bio,
|
||||
theme,
|
||||
language,
|
||||
notifications,
|
||||
auto_sync,
|
||||
)?;
|
||||
}
|
||||
crate::cli::commands::ProfileCommands::Update {
|
||||
file,
|
||||
id,
|
||||
hub_did,
|
||||
handle,
|
||||
display_name,
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
avatar_url,
|
||||
bio,
|
||||
theme,
|
||||
language,
|
||||
notifications,
|
||||
auto_sync,
|
||||
} => {
|
||||
interface.handle_profile_update(
|
||||
&file,
|
||||
&id,
|
||||
hub_did,
|
||||
handle,
|
||||
display_name,
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
avatar_url,
|
||||
bio,
|
||||
theme,
|
||||
language,
|
||||
notifications,
|
||||
auto_sync,
|
||||
)?;
|
||||
}
|
||||
crate::cli::commands::ProfileCommands::Delete { file, id } => {
|
||||
interface.handle_profile_delete(&file, &id)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
105
src/bin/test_universe_binding.rs
Normal file
105
src/bin/test_universe_binding.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
use sharenet_passport::application::use_cases::*;
|
||||
use sharenet_passport::domain::entities::*;
|
||||
use sharenet_passport::infrastructure::crypto::*;
|
||||
use sharenet_passport::infrastructure::storage::*;
|
||||
use std::fs;
|
||||
|
||||
fn main() {
|
||||
println!("Testing universe binding implementation...");
|
||||
|
||||
let mnemonic_generator = Bip39MnemonicGenerator;
|
||||
let key_deriver = Ed25519KeyDeriver;
|
||||
let file_encryptor = XChaCha20FileEncryptor;
|
||||
let file_storage = FileSystemStorage;
|
||||
|
||||
let create_use_case = CreatePassportUseCase::new(
|
||||
mnemonic_generator.clone(),
|
||||
key_deriver.clone(),
|
||||
file_encryptor.clone(),
|
||||
file_storage.clone(),
|
||||
);
|
||||
|
||||
// Test 1: Create passports for different universes
|
||||
println!("\nTest 1: Creating passports for different universes...");
|
||||
let univ1 = "univ:test:alpha";
|
||||
let univ2 = "univ:test:beta";
|
||||
let password = "test_password";
|
||||
|
||||
let (passport1, recovery_phrase) = create_use_case
|
||||
.execute(univ1, password, "/tmp/test_passport1.spf")
|
||||
.expect("Failed to create passport 1");
|
||||
|
||||
println!("✓ Passport 1 created for universe: {}", passport1.univ_id());
|
||||
println!(" DID: {}", passport1.did().as_str());
|
||||
|
||||
let import_use_case = ImportFromRecoveryUseCase::new(
|
||||
mnemonic_generator,
|
||||
key_deriver,
|
||||
file_encryptor,
|
||||
file_storage,
|
||||
);
|
||||
|
||||
let passport2 = import_use_case
|
||||
.execute(
|
||||
univ2,
|
||||
&recovery_phrase.words(),
|
||||
password,
|
||||
"/tmp/test_passport2.spf",
|
||||
)
|
||||
.expect("Failed to create passport 2");
|
||||
|
||||
println!("✓ Passport 2 created for universe: {}", passport2.univ_id());
|
||||
println!(" DID: {}", passport2.did().as_str());
|
||||
|
||||
// Verify universe binding
|
||||
assert_eq!(passport1.univ_id(), univ1);
|
||||
assert_eq!(passport2.univ_id(), univ2);
|
||||
assert_ne!(passport1.did().as_str(), passport2.did().as_str());
|
||||
assert_ne!(
|
||||
hex::encode(&passport1.public_key().0),
|
||||
hex::encode(&passport2.public_key().0)
|
||||
);
|
||||
|
||||
println!("✓ Universe binding verified - different universes produce different identities");
|
||||
|
||||
// Test 2: Universe-bound card signing
|
||||
println!("\nTest 2: Testing universe-bound card signing...");
|
||||
let sign_use_case = SignCardUseCase::new();
|
||||
let message = "Hello, universe!";
|
||||
let signature = sign_use_case
|
||||
.execute(&passport1, message)
|
||||
.expect("Failed to sign message");
|
||||
|
||||
println!("✓ Message signed successfully");
|
||||
|
||||
// Test 3: Verify PassportFile stores univ_id
|
||||
println!("\nTest 3: Verifying PassportFile stores univ_id...");
|
||||
let loaded_file = FileSystemStorage
|
||||
.load("/tmp/test_passport1.spf")
|
||||
.expect("Failed to load passport file");
|
||||
|
||||
assert_eq!(loaded_file.univ_id, univ1);
|
||||
println!("✓ PassportFile correctly stores univ_id: {}", loaded_file.univ_id);
|
||||
|
||||
// Test 4: Import from file preserves univ_id
|
||||
println!("\nTest 4: Testing import from file preserves univ_id...");
|
||||
let import_file_use_case = ImportFromFileUseCase::new(XChaCha20FileEncryptor, FileSystemStorage);
|
||||
let imported_passport = import_file_use_case
|
||||
.execute("/tmp/test_passport1.spf", password, None)
|
||||
.expect("Failed to import passport");
|
||||
|
||||
assert_eq!(imported_passport.univ_id(), univ1);
|
||||
println!("✓ Imported passport preserves univ_id: {}", imported_passport.univ_id());
|
||||
|
||||
// Clean up
|
||||
let _ = fs::remove_file("/tmp/test_passport1.spf");
|
||||
let _ = fs::remove_file("/tmp/test_passport2.spf");
|
||||
|
||||
println!("\n🎉 All universe binding tests passed successfully!");
|
||||
println!("\nSummary:");
|
||||
println!("- Passports are cryptographically bound to their universe");
|
||||
println!("- Same mnemonic + different universe = different identities");
|
||||
println!("- DIDs include universe identifier");
|
||||
println!("- Card signatures are universe-bound");
|
||||
println!("- Passport files store univ_id for verification");
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue