Add to project
Some checks failed
Podman Rootless Demo / test-backend (push) Has been skipped
Podman Rootless Demo / test-frontend (push) Has been skipped
Podman Rootless Demo / build-backend (push) Has been skipped
Podman Rootless Demo / build-frontend (push) Failing after 5m32s
Podman Rootless Demo / deploy-prod (push) Has been skipped

This commit is contained in:
continuist 2025-11-01 11:53:11 -04:00
parent 24392535d8
commit 05674b4caa
41 changed files with 11143 additions and 0 deletions

1282
passport-cli/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

18
passport-cli/Cargo.toml Normal file
View file

@ -0,0 +1,18 @@
[package]
name = "passport-cli"
version = "0.1.0"
edition = "2021"
description = "Sharenet Passport CLI Tool"
authors = ["Your Name <your.email@example.com>"]
[dependencies]
passport = { path = "../passport" }
clap = { version = "4.4", features = ["derive"] }
rpassword = "7.2"
hex = "0.4"
uuid = { version = "1.7", features = ["v7"] }
serde_cbor = "0.11"
[dev-dependencies]
assert_matches = "1.5"
tempfile = "3.8"

View file

@ -0,0 +1,71 @@
use std::fs;
use serde_cbor::Value;
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() != 2 {
eprintln!("Usage: {} <passport-file>", args[0]);
std::process::exit(1);
}
let file_path = &args[1];
let data = fs::read(file_path).expect("Failed to read file");
println!("File size: {} bytes", data.len());
match serde_cbor::from_slice::<Value>(&data) {
Ok(value) => {
println!("\nCBOR structure:");
print_value(&value, 0);
}
Err(e) => {
println!("Failed to parse as CBOR: {}", e);
println!("\nFirst 100 bytes (hex):");
println!("{}", hex::encode(&data[..std::cmp::min(100, data.len())]));
}
}
}
fn print_value(value: &Value, indent: usize) {
let spaces = " ".repeat(indent);
match value {
Value::Map(map) => {
println!("{}Map ({} items):", spaces, map.len());
for (key, val) in map {
print!("{}- ", spaces);
match key {
Value::Text(t) => print!("{}: ", t),
_ => print!("{:?}: ", key),
}
match val {
Value::Bytes(b) => println!("Bytes ({} bytes)", b.len()),
Value::Text(t) => println!("Text: {:?}", t),
Value::Integer(i) => println!("Integer: {}", i),
Value::Array(_) => println!("Array"),
Value::Map(_) => println!("Map"),
_ => println!("{:?}", val),
}
print_value(val, indent + 1);
}
}
Value::Array(arr) => {
println!("{}Array ({} items)", spaces, arr.len());
for (i, item) in arr.iter().enumerate() {
println!("{}[{}]:", spaces, i);
print_value(item, indent + 1);
}
}
Value::Bytes(b) => {
println!("{}Bytes ({} bytes): {}", spaces, b.len(), hex::encode(&b[..std::cmp::min(16, b.len())]));
}
Value::Text(t) => {
println!("{}Text: {:?}", spaces, t);
}
Value::Integer(i) => {
println!("{}Integer: {}", spaces, i);
}
_ => {
println!("{}{:?}", spaces, value);
}
}
}

View file

@ -0,0 +1,106 @@
use passport::{
application::use_cases::*,
Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor, FileSystemStorage,
FileStorage,
};
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");
}

View file

@ -0,0 +1,241 @@
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "sharenet-passport-cli")]
#[command(about = "Sharenet Passport Creator - Generate and manage cryptographic identities")]
#[command(version)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
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,
},
/// Import a Passport from .spf file
ImportFile {
/// Input .spf file path
input: String,
/// Output file path for the re-encrypted .spf file (optional)
#[arg(short, long)]
output: Option<String>,
},
/// Export a Passport to .spf file
Export {
/// Input .spf file path
input: String,
/// Output file path
#[arg(short, long)]
output: String,
},
/// Display Passport information
Info {
/// .spf file path
file: String,
},
/// Display complete decrypted Passport data
Show {
/// .spf file path
file: String,
},
/// Edit global Passport fields
Edit {
/// .spf file path
file: String,
/// Date of birth (format: MM-DD-YYYY)
#[arg(long, conflicts_with = "remove_date_of_birth")]
date_of_birth: Option<String>,
/// Remove date of birth
#[arg(long, conflicts_with = "date_of_birth")]
remove_date_of_birth: bool,
},
/// Sign a message (for testing)
Sign {
/// .spf file path
file: String,
/// 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(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, conflicts_with = "default")]
id: Option<String>,
/// Update the default user profile
#[arg(long, conflicts_with = "id")]
default: bool,
/// 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>,
/// Show date of birth
#[arg(long)]
show_date_of_birth: 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,
},
}

View file

@ -0,0 +1,661 @@
use passport::{
application::use_cases::*,
domain::entities::{UserIdentity, UserPreferences},
Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor, FileSystemStorage,
ApplicationError, FileStorage,
};
use rpassword::prompt_password;
use hex;
use uuid::Uuid;
pub struct CliInterface;
impl CliInterface {
pub fn new() -> Self {
Self
}
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> {
// Validate universe ID format
if !universe.starts_with("u:") {
return Err(ApplicationError::UseCaseError(
"Invalid universe ID format. Must start with 'u:'".to_string()
));
}
let password = prompt_password("Enter password for new passport: ")
.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()));
}
let use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (passport, recovery_phrase) = use_case.execute(universe, &password, output)?;
println!("✅ Passport created successfully!");
println!("📄 Saved to: {}", output);
println!("🔑 Public Key: {}", hex::encode(&passport.public_key().0));
println!("🆔 DID: {}", passport.did().as_str());
println!("\n📝 IMPORTANT: Save your recovery phrase in a secure location!");
println!("Recovery phrase: {}", recovery_phrase.to_string());
Ok(())
}
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))
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read recovery word: {}", e)))?;
// Validate recovery word is not empty
if word.trim().is_empty() {
return Err(ApplicationError::UseCaseError(
format!("Recovery word {} cannot be empty", i)
));
}
recovery_words.push(word.trim().to_lowercase());
}
// Validate that all words are non-empty
if recovery_words.iter().any(|word| word.is_empty()) {
return Err(ApplicationError::UseCaseError(
"Recovery phrase contains empty words".to_string()
));
}
let password = prompt_password("Enter new password for passport file: ")
.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()));
}
let use_case = ImportFromRecoveryUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let passport = use_case.execute(universe, &recovery_words, &password, output)?;
println!("✅ Passport imported successfully!");
println!("📄 Saved to: {}", output);
println!("🔑 Public Key: {}", hex::encode(&passport.public_key().0));
println!("🆔 DID: {}", passport.did().as_str());
Ok(())
}
pub fn handle_import_file(&self, input: &str, output: Option<&str>) -> Result<(), ApplicationError> {
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,
FileSystemStorage,
);
let passport = use_case.execute(input, &password, output)?;
println!("✅ Passport imported successfully!");
if let Some(output_path) = output {
println!("📄 Re-encrypted to: {}", output_path);
}
println!("🔑 Public Key: {}", hex::encode(&passport.public_key().0));
println!("🆔 DID: {}", passport.did().as_str());
Ok(())
}
pub fn handle_export(&self, input: &str, output: &str) -> Result<(), ApplicationError> {
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()));
}
// First import to get the passport
let import_use_case = ImportFromFileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let passport = import_use_case.execute(input, &password, None)?;
// Then export with new password
let export_use_case = ExportPassportUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
export_use_case.execute(&passport, &new_password, output)?;
println!("✅ Passport exported successfully!");
println!("📄 Saved to: {}", output);
Ok(())
}
pub fn handle_info(&self, file: &str) -> Result<(), ApplicationError> {
let passport_file = FileSystemStorage.load(file)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to load file: {}", e)))?;
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);
println!(" Public Key: {}", hex::encode(&passport_file.public_key));
println!(" KDF: {}", passport_file.kdf);
println!(" Cipher: {}", passport_file.cipher);
Ok(())
}
pub fn handle_show(&self, file: &str) -> Result<(), ApplicationError> {
let password = prompt_password("Enter password for passport file: ")
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
let import_use_case = ImportFromFileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let passport = import_use_case.execute(file, &password, None)?;
println!("🔓 Complete Decrypted Passport Data:");
println!(" File: {}", file);
println!(" Universe ID: {}", passport.univ_id());
println!(" DID: {}", passport.did().as_str());
println!(" Public Key: {}", hex::encode(&passport.public_key.0));
println!(" Private Key: {} (⚠️ SENSITIVE - DO NOT SHARE)", hex::encode(&passport.private_key.0));
println!(" Seed: {} (⚠️ SENSITIVE - DO NOT SHARE)", hex::encode(passport.seed.as_bytes()));
if let Some(date_of_birth) = &passport.date_of_birth {
println!(" Date of Birth: {}-{}-{}", date_of_birth.month, date_of_birth.day, date_of_birth.year);
} else {
println!(" Date of Birth: Not set");
}
if let Some(default_profile_id) = &passport.default_user_profile_id {
println!(" Default User Profile ID: {}", default_profile_id);
} else {
println!(" Default User Profile ID: Not set");
}
println!("\n👤 User Profiles ({} total):", passport.user_profiles().len());
for (i, profile) in passport.user_profiles().iter().enumerate() {
println!("\n {}. Profile ID: {}", i + 1, profile.id);
println!(" Profile Type: {}", if profile.is_default() { "Default" } else { "Hub-specific" });
if let Some(hub_did) = &profile.hub_did {
println!(" Hub DID: {}", hub_did);
}
println!(" Created: {}", profile.created_at);
println!(" Updated: {}", profile.updated_at);
println!(" Identity:");
if let Some(handle) = &profile.identity.handle {
println!(" Handle: {}", handle);
}
if let Some(name) = &profile.identity.display_name {
println!(" Display Name: {}", name);
}
if let Some(first_name) = &profile.identity.first_name {
println!(" First Name: {}", first_name);
}
if let Some(last_name) = &profile.identity.last_name {
println!(" Last Name: {}", last_name);
}
if let Some(email) = &profile.identity.email {
println!(" Email: {}", email);
}
if let Some(avatar) = &profile.identity.avatar_url {
println!(" Avatar URL: {}", avatar);
}
if let Some(bio) = &profile.identity.bio {
println!(" Bio: {}", bio);
}
println!(" Preferences:");
if let Some(theme) = &profile.preferences.theme {
println!(" Theme: {}", theme);
}
if let Some(language) = &profile.preferences.language {
println!(" Language: {}", language);
}
println!(" Notifications: {}", if profile.preferences.notifications_enabled { "Enabled" } else { "Disabled" });
println!(" Auto-sync: {}", if profile.preferences.auto_sync { "Enabled" } else { "Disabled" });
println!(" Show Date of Birth: {}", if profile.preferences.show_date_of_birth { "Yes" } else { "No" });
}
println!("\n⚠️ SECURITY WARNING:");
println!(" - Private key and seed are sensitive cryptographic material");
println!(" - Never share these values with anyone");
println!(" - Keep this information secure and confidential");
Ok(())
}
pub fn handle_edit(
&self,
file: &str,
date_of_birth: Option<String>,
remove_date_of_birth: bool,
) -> Result<(), ApplicationError> {
let password = prompt_password("Enter password for passport file: ")
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
let import_use_case = ImportFromFileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let mut passport = import_use_case.execute(file, &password, None)?;
let mut changes_made = false;
// Handle date of birth changes
if remove_date_of_birth {
passport.date_of_birth = None;
changes_made = true;
println!("🗑️ Date of birth removed");
} else if let Some(dob_str) = date_of_birth {
// Parse date of birth string (format: MM-DD-YYYY)
let parts: Vec<&str> = dob_str.split('-').collect();
if parts.len() != 3 {
return Err(ApplicationError::UseCaseError(
"Invalid date format. Use MM-DD-YYYY".to_string()
));
}
let month = parts[0].parse::<u8>()
.map_err(|_| ApplicationError::UseCaseError("Invalid month".to_string()))?;
let day = parts[1].parse::<u8>()
.map_err(|_| ApplicationError::UseCaseError("Invalid day".to_string()))?;
let year = parts[2].parse::<u16>()
.map_err(|_| ApplicationError::UseCaseError("Invalid year".to_string()))?;
// Basic validation
if month < 1 || month > 12 {
return Err(ApplicationError::UseCaseError("Month must be between 1 and 12".to_string()));
}
if day < 1 || day > 31 {
return Err(ApplicationError::UseCaseError("Day must be between 1 and 31".to_string()));
}
if year < 1900 || year > 2100 {
return Err(ApplicationError::UseCaseError("Year must be between 1900 and 2100".to_string()));
}
// Comprehensive date validation
let max_days = match month {
2 => {
// February - check for leap year
let is_leap_year = (year % 4 == 0) && (year % 100 != 0 || year % 400 == 0);
if is_leap_year { 29 } else { 28 }
}
4 | 6 | 9 | 11 => 30, // April, June, September, November
_ => 31, // January, March, May, July, August, October, December
};
if day > max_days {
return Err(ApplicationError::UseCaseError(
format!("Invalid day {} for month {}. Maximum days for this month is {}", day, month, max_days)
));
}
let new_dob = passport::domain::entities::DateOfBirth {
month,
day,
year,
};
passport.date_of_birth = Some(new_dob);
changes_made = true;
println!("📅 Date of birth set to: {}-{}-{}", month, day, year);
}
if !changes_made {
println!(" No changes specified. Use --date-of-birth or --remove-date-of-birth");
return Ok(());
}
// Save the updated passport
let export_use_case = ExportPassportUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
export_use_case.execute(&passport, &password, file)?;
println!("✅ Passport updated successfully!");
println!("📄 Saved to: {}", file);
Ok(())
}
pub fn handle_sign(&self, file: &str, message: &str) -> Result<(), ApplicationError> {
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)?;
let sign_use_case = SignCardUseCase::new();
let signature = sign_use_case.execute(&passport, message)?;
println!("✅ Message signed successfully!");
println!("📝 Message: {}", message);
println!("🔏 Signature: {}", hex::encode(&signature));
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" });
println!(" Show Date of Birth: {}", if profile.preferences.show_date_of_birth { "Yes" } else { "No" });
}
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,
show_date_of_birth: false,
};
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: Option<&str>,
default: bool,
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>,
show_date_of_birth: 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)?;
// Determine which profile to update and get profile ID
let profile_id = if default {
// Update the default profile
let default_profile = passport.default_user_profile()
.ok_or_else(|| ApplicationError::UseCaseError("Default user profile not found".to_string()))?;
Some(default_profile.id.clone())
} else if let Some(id) = id {
// Update specific profile by ID
Some(id.to_string())
} else {
return Err(ApplicationError::UseCaseError(
"Either --id or --default must be specified".to_string()
));
};
// Get existing profile by ID
let existing_profile = passport.user_profile_by_id(&profile_id.clone().unwrap())
.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),
show_date_of_birth: show_date_of_birth.unwrap_or(existing_profile.preferences.show_date_of_birth),
};
// Clone values before using them in multiple places
let identity_clone = identity.clone();
let preferences_clone = preferences.clone();
let hub_did_clone = hub_did.clone();
let hub_did_for_use_case = hub_did.clone();
// Create updated profile with new hub_did if provided
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 = 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,
profile_id.as_deref(),
hub_did_for_use_case,
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(())
}
}

View file

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

File diff suppressed because it is too large Load diff

121
passport-cli/src/main.rs Normal file
View file

@ -0,0 +1,121 @@
mod cli;
use clap::Parser;
use crate::cli::commands::{Cli, Commands};
use crate::cli::interface::CliInterface;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let interface = CliInterface::new();
match cli.command {
Commands::Create { universe, output } => {
interface.handle_create(&universe, &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())?;
}
Commands::Export { input, output } => {
interface.handle_export(&input, &output)?;
}
Commands::Info { file } => {
interface.handle_info(&file)?;
}
Commands::Show { file } => {
interface.handle_show(&file)?;
}
Commands::Edit { file, date_of_birth, remove_date_of_birth } => {
interface.handle_edit(&file, date_of_birth, remove_date_of_birth)?;
}
Commands::Sign { file, message } => {
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,
default,
hub_did,
handle,
display_name,
first_name,
last_name,
email,
avatar_url,
bio,
theme,
language,
notifications,
auto_sync,
show_date_of_birth,
} => {
interface.handle_profile_update(
&file,
id.as_deref(),
default,
hub_did,
handle,
display_name,
first_name,
last_name,
email,
avatar_url,
bio,
theme,
language,
notifications,
auto_sync,
show_date_of_birth,
)?;
}
crate::cli::commands::ProfileCommands::Delete { file, id } => {
interface.handle_profile_delete(&file, &id)?;
}
}
}
}
Ok(())
}

149
passport/ARCHITECTURE.md Normal file
View file

@ -0,0 +1,149 @@
# Sharenet Passport Architecture
## Overview
The Sharenet Passport library provides a unified API for cryptographic operations and file storage that works seamlessly across both native and WASM targets. The architecture uses Rust's conditional compilation to automatically select the appropriate implementation based on the target platform.
## Architecture Diagram
```
┌─────────────────────────────────────────────────────────────┐
│ Public API Surface │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Bip39MnemonicGenerator, Ed25519KeyDeriver, │ │
│ │ XChaCha20FileEncryptor, FileSystemStorage │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
│ │ Crypto │ │ Storage │ │ RNG │ │
│ │ │ │ │ │ Time │ │
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Target-Specific Implementations │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Native (std) │ WASM │ │
│ │ • OsRng │ • getrandom(js) │ │
│ │ • File system │ • LocalStorage │ │
│ │ • SystemTime │ • web-time │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Module Structure
### Core Modules
- **`domain/`**: Domain entities, traits, and error types
- **`application/`**: Use cases and application logic
- **`infrastructure/`**: Platform-specific implementations
### Infrastructure Layer
- **`infrastructure/crypto/`**: Unified cryptographic API
- **`shared/`**: Shared utilities and constants
- **`native/`**: Native implementations using std
- **`wasm/`**: WASM implementations using browser APIs
- **`infrastructure/storage/`**: Unified storage API
- **`native/`**: File system storage
- **`wasm/`**: Browser LocalStorage
- **`infrastructure/rng/`**: Random number generation abstraction
- **`infrastructure/time/`**: Time abstraction
## Target Selection
The library automatically selects implementations based on the target architecture:
- **Native targets** (`x86_64`, `aarch64`, etc.): Use native implementations
- **WASM targets** (`wasm32-unknown-unknown`): Use WASM implementations
### Conditional Compilation
```rust
// In infrastructure/crypto/mod.rs
#[cfg(any(not(target_arch = "wasm32"), feature = "force-native"))]
mod native;
#[cfg(any(target_arch = "wasm32", feature = "force-wasm"))]
mod wasm;
```
## Optional Override Features
For testing and special cases, optional features allow manual override:
- **`force-wasm`**: Use WASM implementation even on native targets
- **`force-native`**: Use native implementation even on WASM targets
These features are mutually exclusive and will trigger a compile error if both are enabled.
## Testing Strategy
### Target-Specific Tests
- **Native tests**: Located in `src/infrastructure/crypto/native_test.rs`
- **WASM tests**: Located in `src/infrastructure/crypto/wasm_test.rs`
### Test Runners
- **Native**: Standard `cargo test`
- **WASM**: `wasm-bindgen-test-runner` configured in `.cargo/config.toml`
## Dependencies
### Target-Specific Dependencies
- **WASM-only**: `gloo-storage`, `web-time`, `getrandom` with JS feature
- **Native-only**: `getrandom` with std feature
- **Shared**: `bip39`, `ed25519-dalek`, `chacha20poly1305`
## Public API Consistency
The public API remains identical regardless of target:
```rust
// Same API on both targets
use sharenet_passport::{
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
};
```
## Development Workflow
### Testing Both Targets
```bash
# Test native
cargo test
# Test WASM
cargo test --target wasm32-unknown-unknown
# Test with override features
cargo test --features force-wasm
cargo test --features force-native
```
### Building for Different Targets
```bash
# Build native
cargo build
# Build WASM
cargo build --target wasm32-unknown-unknown
```
## Adding New Target-Specific Code
1. Add shared logic to the appropriate `shared/` module
2. Implement native version in `native/` module
3. Implement WASM version in `wasm/` module
4. Update the aggregator module to export the unified API
5. Add target-specific tests
## Cryptographic Consistency
All implementations produce identical cryptographic outputs for the same inputs, ensuring cross-platform compatibility.

960
passport/Cargo.lock generated Normal file
View file

@ -0,0 +1,960 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64ct"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
[[package]]
name = "bip39"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d193de1f7487df1914d3a568b772458861d33f9c54249612cc2893d6915054"
dependencies = [
"bitcoin_hashes",
"serde",
"unicode-normalization",
]
[[package]]
name = "bitcoin-internals"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb"
[[package]]
name = "bitcoin_hashes"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b"
dependencies = [
"bitcoin-internals",
"hex-conservative",
]
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
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.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chacha20"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "chacha20poly1305"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20",
"cipher",
"poly1305",
"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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
"zeroize",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"rand_core",
"typenum",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"zeroize",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"pkcs8",
"serde",
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519",
"serde",
"sha2",
"subtle",
"zeroize",
]
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "generic-array"
version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "gloo-storage"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a"
dependencies = [
"gloo-utils",
"js-sys",
"serde",
"serde_json",
"thiserror",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-utils"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
dependencies = [
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "half"
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 = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hex-conservative"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20"
[[package]]
name = "hkdf"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
dependencies = [
"hmac",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
version = "0.3.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "passport"
version = "0.4.0"
dependencies = [
"async-trait",
"base64",
"bip39",
"chacha20poly1305",
"ciborium",
"ed25519-dalek",
"getrandom 0.2.16",
"gloo-storage",
"hex",
"hkdf",
"js-sys",
"rand",
"rand_core",
"serde",
"serde-wasm-bindgen",
"serde_cbor",
"serde_json",
"sha2",
"tempfile",
"thiserror",
"uuid",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-time",
"zeroize",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "poly1305"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.16",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_cbor"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
dependencies = [
"half 1.8.3",
"serde",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"rand_core",
]
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinyvec"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-normalization"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
dependencies = [
"tinyvec",
]
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]]
name = "uuid"
version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [
"getrandom 0.3.4",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0"
dependencies = [
"cfg-if",
"js-sys",
"once_cell",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "zerocopy"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

66
passport/Cargo.toml Normal file
View file

@ -0,0 +1,66 @@
[package]
name = "passport"
version = "0.4.0"
publish = ["sharenet-sh-forgejo"] # Set this to whichever Cargo registry you are publishing to
edition = "2021"
description = "Core library for Sharenet Passport creation and management"
authors = ["Continuist <continuist02@gmail.com>"]
license = "CC-BY-NC-SA-4.0"
repository = "https://git.sharenet.sh/devteam/sharenet/passport"
readme = "README.md"
keywords = ["cryptography", "identity", "passport", "sharenet"]
categories = ["cryptography", "authentication"]
[dependencies]
bip39 = "2.1"
ed25519-dalek = { version = "2.1", features = ["serde"] }
chacha20poly1305 = "0.10"
hkdf = "0.12"
sha2 = "0.10"
rand = "0.8"
rand_core = "0.6"
serde = { version = "1.0", features = ["derive"] }
serde_cbor = "0.11"
thiserror = "1.0"
zeroize = { version = "1.7", features = ["zeroize_derive"] }
hex = "0.4"
ciborium = "0.2"
# Core async support
async-trait = "0.1"
# Dependencies needed for WASM implementation (available for all targets)
base64 = "0.21"
# WASM-specific dependencies
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2", features = ["js"] }
uuid = { version = "1.10", features = ["v7", "js"] }
web-time = "1.1"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
gloo-storage = "0.3"
wasm-bindgen = "0.2.105"
serde-wasm-bindgen = "0.6"
serde_json = "1.0"
# Native dependencies
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
getrandom = { version = "0.2", features = ["std"] }
uuid = { version = "1.10", features = ["v7", "rng"] }
# Dev dependencies for testing
[dev-dependencies]
tempfile = "3.8"
[lib]
crate-type = ["cdylib", "rlib"] # Support both native and WASM
[features]
default = []
std = [] # Standard library support (for native targets)
alloc = [] # No-std with alloc support
# Optional override features for manual platform selection
force-wasm = [] # Force WASM implementation even on native targets
force-native = [] # Force native implementation even on WASM targets

437
passport/LICENSE Normal file
View file

@ -0,0 +1,437 @@
Attribution-NonCommercial-ShareAlike 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
Public License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-NonCommercial-ShareAlike 4.0 International Public License
("Public License"). To the extent this Public License may be
interpreted as a contract, You are granted the Licensed Rights in
consideration of Your acceptance of these terms and conditions, and the
Licensor grants You such rights in consideration of benefits the
Licensor receives from making the Licensed Material available under
these terms and conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. BY-NC-SA Compatible License means a license listed at
creativecommons.org/compatiblelicenses, approved by Creative
Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
e. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
g. License Elements means the license attributes listed in the name
of a Creative Commons Public License. The License Elements of this
Public License are Attribution, NonCommercial, and ShareAlike.
h. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
i. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
j. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
k. NonCommercial means not primarily intended for or directed towards
commercial advantage or monetary compensation. For purposes of
this Public License, the exchange of the Licensed Material for
other material subject to Copyright and Similar Rights by digital
file-sharing or similar means is NonCommercial provided there is
no payment of monetary compensation in connection with the
exchange.
l. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
m. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
n. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part, for NonCommercial purposes only; and
b. produce, reproduce, and Share Adapted Material for
NonCommercial purposes only.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. Additional offer from the Licensor -- Adapted Material.
Every recipient of Adapted Material from You
automatically receives an offer from the Licensor to
exercise the Licensed Rights in the Adapted Material
under the conditions of the Adapter's License You apply.
c. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
b. ShareAlike.
In addition to the conditions in Section 3(a), if You Share
Adapted Material You produce, the following conditions also apply.
1. The Adapter's License You apply must be a Creative Commons
license with the same License Elements, this version or
later, or a BY-NC-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the
Adapter's License You apply. You may satisfy this condition
in any reasonable manner based on the medium, means, and
context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms
or conditions on, or apply any Effective Technological
Measures to, Adapted Material that restrict exercise of the
rights granted under the Adapter's License You apply.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database for NonCommercial purposes
only;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material,
including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public licenses.
Notwithstanding, Creative Commons may elect to apply one of its public
licenses to material it publishes and in those instances will be
considered the "Licensor." The text of the Creative Commons public
licenses is dedicated to the public domain under the CC0 Public
Domain Dedication. Except for the limited purpose of indicating that
material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the public
licenses.
Creative Commons may be contacted at creativecommons.org.

180
passport/README.md Normal file
View file

@ -0,0 +1,180 @@
# Sharenet Passport Library
A secure Rust library for creating and managing Sharenet Passport files (.spf) for decentralized identity management.
## Features
- **Secure Passport Creation**: Generate encrypted .spf files with BIP-39 mnemonic recovery phrases
- **Ed25519 Key Generation**: Cryptographically secure key derivation and signing
- **Recovery Support**: Import passports from recovery phrases or existing .spf files
- **Export & Re-encrypt**: Export passports with new passwords
- **Message Signing**: Sign messages using your passport's private key
- **Security First**: Zeroize memory management and secure file encryption
- **WASM Support**: Compatible with web applications via WebAssembly
## Installation
### From Private Registry
```toml
[dependencies]
sharenet-passport = { version = "0.2.0", registry = "sharenet-sh-forgejo" }
```
Platform selection is automatic based on your compilation target:
- **Native targets** (x86_64, aarch64, etc.): Use native implementations
- **WASM targets** (wasm32-unknown-unknown): Use WASM implementations
## Usage
### Creating a New Passport
```rust
use sharenet_passport::{
application::use_cases::CreatePassportUseCase,
infrastructure::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor, FileSystemStorage},
};
let use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (passport, recovery_phrase) = use_case.execute("your-password", "passport.spf")?;
println!("Public Key: {:?}", passport.public_key());
println!("DID: {}", passport.did().as_str());
println!("Recovery Phrase: {}", recovery_phrase.to_string());
```
### Importing from Recovery Phrase
```rust
use sharenet_passport::{
application::use_cases::ImportFromRecoveryUseCase,
infrastructure::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor, FileSystemStorage},
};
let use_case = ImportFromRecoveryUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let recovery_words = vec!["word1".to_string(), "word2".to_string(), /* ... 24 words */];
let passport = use_case.execute(&recovery_words, "new-password", "recovered-passport.spf")?;
```
### Signing Messages
```rust
use sharenet_passport::{
application::use_cases::{ImportFromFileUseCase, SignCardUseCase},
infrastructure::{XChaCha20FileEncryptor, FileSystemStorage},
};
// Import passport from file
let import_use_case = ImportFromFileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let passport = import_use_case.execute("passport.spf", "password", None)?;
// Sign message
let sign_use_case = SignCardUseCase::new();
let signature = sign_use_case.execute(&passport, "Hello, Sharenet!")?;
```
## Architecture
Built with Clean Architecture principles:
- **Domain Layer**: Core entities (Passport, RecoveryPhrase, PublicKey, etc.) and traits
- **Application Layer**: Use cases (CreatePassport, ImportFromRecovery, SignCard, etc.)
- **Infrastructure Layer**: Crypto implementations, file storage
## Targets
The library automatically selects the appropriate implementation based on your compilation target:
### Native Targets
- **Platforms**: Linux, macOS, Windows, etc.
- **Storage**: File system
- **RNG**: System entropy (OsRng)
- **Time**: System time
### WASM Targets
- **Platforms**: Web browsers, Node.js
- **Storage**: Browser LocalStorage
- **RNG**: Web Crypto API via getrandom
- **Time**: JavaScript Date API
### Optional Override Features
For testing and special cases:
- `force-wasm`: Use WASM implementation even on native targets
- `force-native`: Use native implementation even on WASM targets
These features are mutually exclusive and will trigger a compile error if both are enabled.
## Security Features
- **XChaCha20-Poly1305**: Authenticated encryption for file security
- **HKDF-SHA256**: Key derivation from passwords
- **Zeroize**: Secure memory wiping for sensitive data
- **BIP-39**: Standard mnemonic generation and validation
- **Ed25519**: Cryptographically secure signing
## File Format (.spf)
Sharenet Passport Files (.spf) are encrypted containers that store:
- **Encrypted Seed**: The master seed encrypted with XChaCha20-Poly1305
- **Public Key**: Your Ed25519 public key
- **DID**: Your Decentralized Identifier
- **Metadata**: Creation timestamp, version, and encryption parameters
## Development
### Running Tests
```bash
# Test native implementation
cargo test
# Test WASM implementation
cargo test --target wasm32-unknown-unknown
# Test with override features
cargo test --features force-wasm
cargo test --features force-native
```
### Building for Different Targets
```bash
# Build for native
cargo build
# Build for WASM
cargo build --target wasm32-unknown-unknown
```
## License
This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
You are free to:
- **Share** — copy and redistribute the material in any medium or format
- **Adapt** — remix, transform, and build upon the material
Under the following terms:
- **Attribution** — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
- **NonCommercial** — You may not use the material for commercial purposes.
- **ShareAlike** — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/

View file

@ -0,0 +1,10 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ApplicationError {
#[error("Use case error: {0}")]
UseCaseError(String),
#[error("Domain error: {0}")]
DomainError(#[from] crate::domain::error::DomainError),
}

View file

@ -0,0 +1,5 @@
pub mod use_cases;
pub mod error;
#[cfg(test)]
pub mod use_cases_test;

View file

@ -0,0 +1,535 @@
use crate::domain::entities::*;
use crate::domain::traits::*;
use crate::application::error::ApplicationError;
use ed25519_dalek::Signer;
use crate::infrastructure::time;
pub struct CreatePassportUseCase<MG, KD, FE, FS>
where
MG: MnemonicGenerator,
KD: KeyDeriver,
FE: FileEncryptor,
FS: FileStorage,
{
mnemonic_generator: MG,
key_deriver: KD,
file_encryptor: FE,
file_storage: FS,
}
impl<MG, KD, FE, FS> CreatePassportUseCase<MG, KD, FE, FS>
where
MG: MnemonicGenerator,
KD: KeyDeriver,
FE: FileEncryptor,
FS: FileStorage,
{
pub fn new(
mnemonic_generator: MG,
key_deriver: KD,
file_encryptor: FE,
file_storage: FS,
) -> Self {
Self {
mnemonic_generator,
key_deriver,
file_encryptor,
file_storage,
}
}
pub fn execute(
&self,
univ_id: &str,
password: &str,
output_path: &str,
) -> Result<(Passport, RecoveryPhrase), ApplicationError> {
// Generate recovery phrase
let recovery_phrase = self
.mnemonic_generator
.generate()
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to generate mnemonic: {}", e.into())))?;
// Derive seed from mnemonic and universe
let seed = self
.key_deriver
.derive_from_mnemonic(&recovery_phrase, univ_id)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to derive seed: {}", e.into())))?;
// Derive keys from seed
let (public_key, private_key) = self
.key_deriver
.derive_from_seed(&seed)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to derive keys: {}", e.into())))?;
// Create passport (without storing recovery phrase)
let passport = Passport::new(
seed,
public_key,
private_key,
univ_id.to_string(),
);
// Encrypt and save file
let passport_file = self
.file_encryptor
.encrypt(
&passport.seed,
password,
&passport.public_key,
&passport.did,
&passport.univ_id,
&passport.user_profiles,
&passport.date_of_birth,
&passport.default_user_profile_id,
)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
self.file_storage
.save(&passport_file, output_path)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to save file: {}", e.into())))?;
Ok((passport, recovery_phrase))
}
}
pub struct ImportFromRecoveryUseCase<MG, KD, FE, FS>
where
MG: MnemonicGenerator,
KD: KeyDeriver,
FE: FileEncryptor,
FS: FileStorage,
{
mnemonic_generator: MG,
key_deriver: KD,
file_encryptor: FE,
file_storage: FS,
}
impl<MG, KD, FE, FS> ImportFromRecoveryUseCase<MG, KD, FE, FS>
where
MG: MnemonicGenerator,
KD: KeyDeriver,
FE: FileEncryptor,
FS: FileStorage,
{
pub fn new(
mnemonic_generator: MG,
key_deriver: KD,
file_encryptor: FE,
file_storage: FS,
) -> Self {
Self {
mnemonic_generator,
key_deriver,
file_encryptor,
file_storage,
}
}
pub fn execute(
&self,
univ_id: &str,
recovery_words: &[String],
password: &str,
output_path: &str,
) -> Result<Passport, ApplicationError> {
// Validate recovery phrase
self.mnemonic_generator
.validate(recovery_words)
.map_err(|e| ApplicationError::UseCaseError(format!("Invalid recovery phrase: {}", e.into())))?;
let recovery_phrase = RecoveryPhrase::new(recovery_words.to_vec());
// Derive seed from mnemonic and universe
let seed = self
.key_deriver
.derive_from_mnemonic(&recovery_phrase, univ_id)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to derive seed: {}", e.into())))?;
// Derive keys from seed
let (public_key, private_key) = self
.key_deriver
.derive_from_seed(&seed)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to derive keys: {}", e.into())))?;
// Create passport (without storing recovery phrase)
let passport = Passport::new(
seed,
public_key,
private_key,
univ_id.to_string(),
);
// Encrypt and save file
let passport_file = self
.file_encryptor
.encrypt(
&passport.seed,
password,
&passport.public_key,
&passport.did,
&passport.univ_id,
&passport.user_profiles,
&passport.date_of_birth,
&passport.default_user_profile_id,
)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
self.file_storage
.save(&passport_file, output_path)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to save file: {}", e.into())))?;
Ok(passport)
}
}
pub struct ImportFromFileUseCase<FE, FS>
where
FE: FileEncryptor,
FS: FileStorage,
{
file_encryptor: FE,
file_storage: FS,
}
impl<FE, FS> ImportFromFileUseCase<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,
file_path: &str,
password: &str,
output_path: Option<&str>,
) -> Result<Passport, ApplicationError> {
// Load encrypted file
let passport_file = self
.file_storage
.load(file_path)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to load file: {}", e.into())))?;
// Decrypt file
let (seed, public_key, private_key, user_profiles, date_of_birth, default_user_profile_id) = 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 mut passport = Passport::new(
seed,
public_key,
private_key,
passport_file.univ_id.clone(),
);
passport.user_profiles = user_profiles;
passport.date_of_birth = date_of_birth;
passport.default_user_profile_id = default_user_profile_id;
// Re-encrypt and save if output path provided
if let Some(output_path) = output_path {
let new_passport_file = self
.file_encryptor
.encrypt(
&passport.seed,
password,
&passport.public_key,
&passport.did,
&passport.univ_id,
&passport.user_profiles,
&passport.date_of_birth,
&passport.default_user_profile_id,
)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to re-encrypt file: {}", e.into())))?;
self.file_storage
.save(&new_passport_file, output_path)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to save file: {}", e.into())))?;
}
Ok(passport)
}
}
pub struct ExportPassportUseCase<FE, FS>
where
FE: FileEncryptor,
FS: FileStorage,
{
file_encryptor: FE,
file_storage: FS,
}
impl<FE, FS> ExportPassportUseCase<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: &Passport,
password: &str,
output_path: &str,
) -> Result<(), ApplicationError> {
let passport_file = self
.file_encryptor
.encrypt(
&passport.seed,
password,
&passport.public_key,
&passport.did,
&passport.univ_id,
&passport.user_profiles,
&passport.date_of_birth,
&passport.default_user_profile_id,
)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to encrypt file: {}", e.into())))?;
self.file_storage
.save(&passport_file, output_path)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to save file: {}", e.into())))?;
Ok(())
}
}
pub struct SignCardUseCase;
impl SignCardUseCase {
pub fn new() -> Self {
Self
}
pub fn execute(
&self,
passport: &Passport,
message: &str,
) -> Result<Vec<u8>, ApplicationError> {
// Convert the private key bytes to an ed25519_dalek SigningKey
let signing_key = ed25519_dalek::SigningKey::from_bytes(
&passport.private_key.0[..]
.try_into()
.map_err(|_| ApplicationError::UseCaseError("Invalid private key length".to_string()))?
);
// 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,
&passport.date_of_birth,
&passport.default_user_profile_id,
)
.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>,
hub_did: Option<String>,
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 = time::now_seconds()
.map_err(|e| ApplicationError::UseCaseError(format!("Time error: {}", e)))?;
// Use provided hub_did or keep existing
let profile = 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,
};
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,
&passport.date_of_birth,
&passport.default_user_profile_id,
)
.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,
&passport.date_of_birth,
&passport.default_user_profile_id,
)
.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(())
}
}

View file

@ -0,0 +1,533 @@
use tempfile::NamedTempFile;
use crate::application::use_cases::*;
use crate::domain::entities::*;
use crate::domain::traits::FileStorage;
use crate::infrastructure::*;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_user_profile_use_case() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a passport first
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (mut passport, _) = create_use_case.execute("test-universe", "test-password", file_path)
.expect("Failed to create passport");
// Test creating a user profile
let create_profile_use_case = CreateUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
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,
show_date_of_birth: false,
};
let result = create_profile_use_case.execute(
&mut passport,
Some("h:example".to_string()),
identity,
preferences,
"test-password",
file_path,
);
assert!(result.is_ok());
// Verify the profile was added
assert_eq!(passport.user_profiles.len(), 2); // default + new profile
let hub_profile = passport.user_profile_for_hub("h:example");
assert!(hub_profile.is_some());
assert_eq!(hub_profile.unwrap().identity.handle, Some("testuser".to_string()));
}
#[test]
fn test_create_user_profile_duplicate_hub_did() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a passport first
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (mut passport, _) = create_use_case.execute("test-universe", "test-password", file_path)
.expect("Failed to create passport");
// Create first profile
let create_profile_use_case = CreateUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let identity1 = UserIdentity {
handle: Some("user1".to_string()),
display_name: Some("User One".to_string()),
first_name: Some("User".to_string()),
last_name: Some("One".to_string()),
email: Some("user1@example.com".to_string()),
avatar_url: None,
bio: None,
};
let preferences1 = UserPreferences {
theme: Some("dark".to_string()),
language: Some("en".to_string()),
notifications_enabled: true,
auto_sync: false,
show_date_of_birth: false,
};
let result1 = create_profile_use_case.execute(
&mut passport,
Some("h:example".to_string()),
identity1,
preferences1,
"test-password",
file_path,
);
assert!(result1.is_ok());
// Try to create second profile with same hub DID (should fail)
let identity2 = UserIdentity {
handle: Some("user2".to_string()),
display_name: Some("User Two".to_string()),
first_name: Some("User".to_string()),
last_name: Some("Two".to_string()),
email: Some("user2@example.com".to_string()),
avatar_url: None,
bio: None,
};
let preferences2 = UserPreferences {
theme: Some("light".to_string()),
language: Some("es".to_string()),
notifications_enabled: false,
auto_sync: true,
show_date_of_birth: false,
};
let result2 = create_profile_use_case.execute(
&mut passport,
Some("h:example".to_string()),
identity2,
preferences2,
"test-password",
file_path,
);
assert!(result2.is_err());
}
#[test]
fn test_update_user_profile_use_case() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a passport first
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (mut passport, _) = create_use_case.execute("test-universe", "test-password", file_path)
.expect("Failed to create passport");
// Create a user profile first
let create_profile_use_case = CreateUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
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,
show_date_of_birth: false,
};
create_profile_use_case.execute(
&mut passport,
Some("h:example".to_string()),
identity,
preferences,
"test-password",
file_path,
).expect("Failed to create profile");
// Get the profile ID
let profile_id = passport.user_profile_for_hub("h:example")
.expect("Profile should exist")
.id
.clone();
// Test updating the user profile
let update_profile_use_case = UpdateUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
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("light".to_string()),
language: Some("es".to_string()),
notifications_enabled: false,
auto_sync: true,
show_date_of_birth: false,
};
let result = update_profile_use_case.execute(
&mut passport,
Some(&profile_id),
Some("h:example".to_string()),
updated_identity,
updated_preferences,
"test-password",
file_path,
);
assert!(result.is_ok());
// Verify the profile was updated
let updated_profile = passport.user_profile_for_hub("h:example")
.expect("Profile should exist");
assert_eq!(updated_profile.identity.handle, Some("updateduser".to_string()));
assert_eq!(updated_profile.preferences.theme, Some("light".to_string()));
assert_eq!(updated_profile.preferences.language, Some("es".to_string()));
}
#[test]
fn test_update_user_profile_use_case_invalid_id() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a passport first
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (mut passport, _) = create_use_case.execute("test-universe", "test-password", file_path)
.expect("Failed to create passport");
// Try to update non-existent profile (should fail)
let update_profile_use_case = UpdateUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
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: None,
bio: None,
};
let preferences = UserPreferences {
theme: Some("dark".to_string()),
language: Some("en".to_string()),
notifications_enabled: true,
auto_sync: false,
show_date_of_birth: false,
};
let result = update_profile_use_case.execute(
&mut passport,
Some("non-existent-id"),
Some("h:example".to_string()),
identity,
preferences,
"test-password",
file_path,
);
assert!(result.is_err());
}
#[test]
fn test_delete_user_profile_use_case() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a passport first
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (mut passport, _) = create_use_case.execute("test-universe", "test-password", file_path)
.expect("Failed to create passport");
// Create a user profile first
let create_profile_use_case = CreateUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
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: None,
bio: None,
};
let preferences = UserPreferences {
theme: Some("dark".to_string()),
language: Some("en".to_string()),
notifications_enabled: true,
auto_sync: false,
show_date_of_birth: false,
};
create_profile_use_case.execute(
&mut passport,
Some("h:example".to_string()),
identity,
preferences,
"test-password",
file_path,
).expect("Failed to create profile");
// Get the profile ID
let profile_id = passport.user_profile_for_hub("h:example")
.expect("Profile should exist")
.id
.clone();
// Test deleting the user profile
let delete_profile_use_case = DeleteUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let result = delete_profile_use_case.execute(
&mut passport,
Some(&profile_id),
"test-password",
file_path,
);
assert!(result.is_ok());
// Verify the profile was deleted
assert_eq!(passport.user_profiles.len(), 1); // only default profile remains
let deleted_profile = passport.user_profile_for_hub("h:example");
assert!(deleted_profile.is_none());
}
#[test]
fn test_delete_user_profile_use_case_invalid_id() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a passport first
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (mut passport, _) = create_use_case.execute("test-universe", "test-password", file_path)
.expect("Failed to create passport");
// Try to delete non-existent profile (should fail)
let delete_profile_use_case = DeleteUserProfileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let result = delete_profile_use_case.execute(
&mut passport,
Some("non-existent-id"),
"test-password",
file_path,
);
assert!(result.is_err());
}
#[test]
fn test_change_passport_password_workflow() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a passport with old password
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (passport, _) = create_use_case.execute("test-universe", "old-password", file_path)
.expect("Failed to create passport");
// Export passport with new password (simulating password change)
let export_use_case = ExportPassportUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let result = export_use_case.execute(&passport, "new-password", file_path);
assert!(result.is_ok());
// Verify we can import with new password
let import_use_case = ImportFromFileUseCase::new(
XChaCha20FileEncryptor,
FileSystemStorage,
);
let imported_passport = import_use_case.execute(file_path, "new-password", None)
.expect("Failed to import with new password");
// Verify the imported passport has the same DID
assert_eq!(passport.did.as_str(), imported_passport.did.as_str());
assert_eq!(passport.univ_id, imported_passport.univ_id);
}
#[test]
fn test_get_passport_metadata_functionality() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a passport
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
let (passport, _) = create_use_case.execute("test-universe", "test-password", file_path)
.expect("Failed to create passport");
// Load file directly to get metadata
let file_storage = FileSystemStorage;
let passport_file = file_storage.load(file_path)
.expect("Failed to load passport file");
// Verify metadata fields
assert!(!passport_file.did.is_empty());
assert!(!passport_file.univ_id.is_empty());
assert!(!passport_file.public_key.is_empty());
assert!(!passport_file.enc_seed.is_empty());
assert!(!passport_file.salt.is_empty());
assert!(!passport_file.nonce.is_empty());
// Verify DID matches
assert_eq!(passport_file.did, passport.did.as_str());
assert_eq!(passport_file.univ_id, passport.univ_id);
}
#[test]
fn test_validate_passport_file_functionality() {
// Create a temporary file for testing
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_str().unwrap();
// Create a valid passport
let create_use_case = CreatePassportUseCase::new(
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
);
create_use_case.execute("test-universe", "test-password", file_path)
.expect("Failed to create passport");
// Load file directly to validate
let file_storage = FileSystemStorage;
let passport_file = file_storage.load(file_path)
.expect("Failed to load passport file");
// Validate the file structure
let is_valid = !passport_file.enc_seed.is_empty()
&& !passport_file.salt.is_empty()
&& !passport_file.nonce.is_empty()
&& !passport_file.public_key.is_empty()
&& !passport_file.did.is_empty()
&& !passport_file.univ_id.is_empty();
assert!(is_valid);
}
#[test]
fn test_validate_passport_file_invalid_file() {
// Try to load non-existent file (should fail)
let file_storage = FileSystemStorage;
let result = file_storage.load("/non/existent/path.spf");
assert!(result.is_err());
}
}

View file

@ -0,0 +1,343 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::infrastructure::time;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecoveryPhrase {
words: Vec<String>,
}
impl RecoveryPhrase {
pub fn new(words: Vec<String>) -> Self {
Self { words }
}
pub fn words(&self) -> &[String] {
&self.words
}
pub fn to_string(&self) -> String {
self.words.join(" ")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublicKey(pub Vec<u8>);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrivateKey(pub Vec<u8>);
impl Zeroize for PrivateKey {
fn zeroize(&mut self) {
self.0.zeroize();
}
}
impl Drop for PrivateKey {
fn drop(&mut self) {
self.zeroize();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Did(pub String);
impl Did {
pub fn new(public_key: &PublicKey) -> Self {
// Passport DID format with "p:" prefix
let did_str = format!("p:{}", hex::encode(&public_key.0));
Self(did_str)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct Seed {
bytes: Vec<u8>,
}
impl Seed {
pub fn new(bytes: Vec<u8>) -> Self {
Self { bytes }
}
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Passport {
pub seed: Seed,
pub public_key: PublicKey,
pub private_key: PrivateKey,
pub did: Did,
pub univ_id: String,
pub user_profiles: Vec<UserProfile>,
pub date_of_birth: Option<DateOfBirth>,
pub default_user_profile_id: Option<String>, // UUIDv7 of the default user profile
}
impl Passport {
pub fn new(
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,
show_date_of_birth: false,
},
);
Self {
seed,
public_key,
private_key,
did,
univ_id,
user_profiles: vec![default_profile.clone()],
date_of_birth: None,
default_user_profile_id: Some(default_profile.id.clone()),
}
}
pub fn public_key(&self) -> &PublicKey {
&self.public_key
}
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> {
if let Some(default_id) = &self.default_user_profile_id {
self.user_profile_by_id(default_id)
} else {
// Fallback to implicit detection for backward compatibility
self.user_profiles.iter().find(|p| p.is_default())
}
}
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> {
// If this is a default profile (no hub_did), set it as the default
if profile.hub_did.is_none() {
if self.default_user_profile_id.is_some() {
return Err("Default user profile already exists".to_string());
}
self.default_user_profile_id = Some(profile.id.clone());
}
// 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.default_user_profile_id.as_deref() == Some(profile_id) {
return Err("Cannot delete default user profile".to_string());
}
self.user_profiles.remove(idx);
Ok(())
}
None => Err("User profile not found".to_string()),
}
}
pub fn set_default_user_profile(&mut self, profile_id: &str) -> Result<(), String> {
// Verify the profile exists
if self.user_profile_by_id(profile_id).is_none() {
return Err("User profile not found".to_string());
}
// Verify the profile is a default profile (no hub_did)
if let Some(profile) = self.user_profile_by_id(profile_id) {
if profile.hub_did.is_some() {
return Err("Cannot set hub-specific profile as default".to_string());
}
}
self.default_user_profile_id = Some(profile_id.to_string());
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
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,
pub show_date_of_birth: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DateOfBirth {
pub month: u8,
pub day: u8,
pub year: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
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 = time::now_seconds().unwrap_or_default();
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)]
pub struct PassportFile {
pub enc_seed: Vec<u8>,
pub kdf: String,
pub cipher: String,
pub salt: Vec<u8>,
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>
#[serde(default)]
pub enc_date_of_birth: Vec<u8>, // Encrypted CBOR of Option<DateOfBirth>
#[serde(default)]
pub enc_default_user_profile_id: Vec<u8>, // Encrypted CBOR of Option<String>
}

View file

@ -0,0 +1,253 @@
#[cfg(test)]
mod tests {
use crate::domain::entities::{RecoveryPhrase, PublicKey, Did, Seed, PrivateKey};
use zeroize::Zeroize;
#[test]
fn test_recovery_phrase_creation() {
let words = vec![
"word1".to_string(),
"word2".to_string(),
"word3".to_string(),
];
let phrase = RecoveryPhrase::new(words.clone());
assert_eq!(phrase.words(), &words);
assert_eq!(phrase.to_string(), "word1 word2 word3");
}
#[test]
fn test_did_generation() {
let public_key = PublicKey(vec![1, 2, 3, 4, 5]);
let did = Did::new(&public_key);
// 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]
fn test_seed_zeroization() {
let mut seed = Seed::new(vec![1, 2, 3, 4, 5]);
// Access the bytes
let bytes = seed.as_bytes();
assert_eq!(bytes, &[1, 2, 3, 4, 5]);
// Zeroize should clear the data
seed.zeroize();
// After zeroization, bytes should be empty (zeroize clears the vector)
assert_eq!(seed.as_bytes(), &[] as &[u8]);
}
#[test]
fn test_private_key_zeroization() {
let mut private_key = PrivateKey(vec![1, 2, 3, 4, 5]);
// Zeroize should clear the data
private_key.zeroize();
// After zeroization, bytes should be empty (zeroize clears the vector)
assert_eq!(private_key.0, vec![] as Vec<u8>);
}
#[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,
show_date_of_birth: 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,
show_date_of_birth: false,
},
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,
show_date_of_birth: false,
},
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,
show_date_of_birth: 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,
show_date_of_birth: 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());
}
}

View file

@ -0,0 +1,16 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DomainError {
#[error("Cryptographic error: {0}")]
CryptographicError(String),
#[error("Invalid mnemonic: {0}")]
InvalidMnemonic(String),
#[error("Invalid file format: {0}")]
InvalidFileFormat(String),
#[error("File operation failed: {0}")]
FileOperationError(String),
}

View file

@ -0,0 +1,6 @@
pub mod entities;
pub mod traits;
pub mod error;
#[cfg(test)]
mod entities_test;

View file

@ -0,0 +1,45 @@
use crate::domain::entities::*;
use crate::domain::error::DomainError;
pub trait MnemonicGenerator {
type Error: Into<DomainError>;
fn generate(&self) -> Result<RecoveryPhrase, Self::Error>;
fn validate(&self, words: &[String]) -> Result<(), Self::Error>;
}
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, univ_id: &str) -> Result<Seed, Self::Error>;
}
pub trait FileEncryptor {
type Error: Into<DomainError>;
fn encrypt(
&self,
seed: &Seed,
password: &str,
public_key: &PublicKey,
did: &Did,
univ_id: &str,
user_profiles: &[UserProfile],
date_of_birth: &Option<DateOfBirth>,
default_user_profile_id: &Option<String>,
) -> Result<PassportFile, Self::Error>;
fn decrypt(
&self,
file: &PassportFile,
password: &str,
) -> Result<(Seed, PublicKey, PrivateKey, Vec<UserProfile>, Option<DateOfBirth>, Option<String>), Self::Error>;
}
pub trait FileStorage {
type Error: Into<DomainError>;
fn save(&self, file: &PassportFile, path: &str) -> Result<(), Self::Error>;
fn load(&self, path: &str) -> Result<PassportFile, Self::Error>;
}

View file

@ -0,0 +1,32 @@
//! Unified cryptographic API with target-specific implementations
// Check for mutually exclusive override features
#[cfg(all(feature = "force-wasm", feature = "force-native"))]
compile_error!("Features 'force-wasm' and 'force-native' are mutually exclusive");
// Shared helper functions and types
mod shared;
// Platform-specific implementations
#[cfg(all(not(target_arch = "wasm32"), not(feature = "force-wasm")))]
mod native;
#[cfg(any(target_arch = "wasm32", feature = "force-wasm"))]
mod wasm;
// Re-export the unified API
#[cfg(all(not(target_arch = "wasm32"), not(feature = "force-wasm")))]
pub use native::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor};
#[cfg(any(target_arch = "wasm32", feature = "force-wasm"))]
pub use wasm::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor};
// Target-specific tests
#[cfg(all(test, all(not(target_arch = "wasm32"), not(feature = "force-wasm"))))]
mod native_test;
#[cfg(all(test, any(target_arch = "wasm32", feature = "force-wasm")))]
mod wasm_test;
// Re-export shared types if any
pub use shared::*;

View file

@ -0,0 +1,218 @@
//! Native (std) cryptographic implementations
use bip39::Mnemonic;
use ed25519_dalek::{SigningKey, SECRET_KEY_LENGTH};
use chacha20poly1305::{aead::{Aead, KeyInit}, XChaCha20Poly1305, Key, XNonce};
use hkdf::Hkdf;
use sha2::Sha256;
use rand::{RngCore, rngs::OsRng};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::domain::entities::*;
use crate::domain::error::DomainError;
use crate::domain::traits::*;
use super::shared::*;
#[derive(Clone)]
pub struct Bip39MnemonicGenerator;
impl MnemonicGenerator for Bip39MnemonicGenerator {
type Error = DomainError;
fn generate(&self) -> Result<RecoveryPhrase, Self::Error> {
let mut entropy = [0u8; 32];
OsRng.fill_bytes(&mut entropy);
let mnemonic = Mnemonic::from_entropy(&entropy)
.map_err(|e| DomainError::CryptographicError(format!("Failed to generate mnemonic: {}", e)))?;
let words: Vec<String> = mnemonic.words().into_iter().map(|s| s.to_string()).collect();
Ok(RecoveryPhrase::new(words))
}
fn validate(&self, words: &[String]) -> Result<(), Self::Error> {
let phrase = words.join(" ");
Mnemonic::parse(&phrase)
.map_err(|e| DomainError::InvalidMnemonic(format!("Invalid mnemonic: {}", e)))?;
Ok(())
}
}
#[derive(Clone)]
pub struct Ed25519KeyDeriver;
impl KeyDeriver for Ed25519KeyDeriver {
type Error = DomainError;
fn derive_from_seed(&self, seed: &Seed) -> Result<(PublicKey, PrivateKey), Self::Error> {
validate_seed_length(seed.as_bytes())?;
let signing_key = SigningKey::from_bytes(&seed.as_bytes()[..SECRET_KEY_LENGTH].try_into()
.map_err(|_| DomainError::CryptographicError("Invalid seed length".to_string()))?);
let verifying_key = signing_key.verifying_key();
Ok((
PublicKey(verifying_key.to_bytes().to_vec()),
PrivateKey(signing_key.to_bytes().to_vec()),
))
}
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 univ_id as passphrase to bind seed to universe
let bip39_seed = bip39_mnemonic.to_seed(univ_id);
// BIP39 produces 64-byte seed, but we only need 32 bytes for Ed25519
// Use the first 32 bytes of the BIP39 seed
let ed25519_seed: [u8; 32] = bip39_seed[..32]
.try_into()
.map_err(|_| DomainError::CryptographicError("Failed to extract 32-byte seed from BIP39 seed".to_string()))?;
Ok(Seed::new(ed25519_seed.to_vec()))
}
}
#[derive(Clone)]
pub struct XChaCha20FileEncryptor;
impl FileEncryptor for XChaCha20FileEncryptor {
type Error = DomainError;
fn encrypt(
&self,
seed: &Seed,
password: &str,
public_key: &PublicKey,
did: &Did,
univ_id: &str,
user_profiles: &[UserProfile],
date_of_birth: &Option<DateOfBirth>,
default_user_profile_id: &Option<String>,
) -> Result<PassportFile, Self::Error> {
// Generate salt and nonce
let mut salt = [0u8; SALT_LENGTH];
let mut nonce_bytes = [0u8; NONCE_LENGTH];
OsRng.fill_bytes(&mut salt);
OsRng.fill_bytes(&mut nonce_bytes);
// Derive KEK from password using HKDF
let hk = Hkdf::<Sha256>::new(Some(&salt), password.as_bytes());
let mut kek = [0u8; 32];
hk.expand(KDF_INFO, &mut kek)
.map_err(|e| DomainError::CryptographicError(format!("HKDF failed: {}", e)))?;
// Encrypt seed
let cipher = XChaCha20Poly1305::new(&Key::from(kek));
let nonce = XNonce::from_slice(&nonce_bytes);
let enc_seed = cipher
.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)))?;
// Serialize and encrypt date of birth
let date_of_birth_bytes = serde_cbor::to_vec(&date_of_birth)
.map_err(|e| DomainError::CryptographicError(format!("Failed to serialize date of birth: {}", e)))?;
let enc_date_of_birth = cipher
.encrypt(&nonce, &*date_of_birth_bytes)
.map_err(|e| DomainError::CryptographicError(format!("Date of birth encryption failed: {}", e)))?;
// Serialize and encrypt default user profile ID
let default_user_profile_id_bytes = serde_cbor::to_vec(&default_user_profile_id)
.map_err(|e| DomainError::CryptographicError(format!("Failed to serialize default user profile ID: {}", e)))?;
let enc_default_user_profile_id = cipher
.encrypt(&nonce, &*default_user_profile_id_bytes)
.map_err(|e| DomainError::CryptographicError(format!("Default user profile ID encryption failed: {}", e)))?;
// Get current timestamp
let created_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| DomainError::CryptographicError(format!("Time error: {}", e)))?
.as_secs();
Ok(PassportFile {
enc_seed,
kdf: KDF_HKDF_SHA256.to_string(),
cipher: CIPHER_XCHACHA20_POLY1305.to_string(),
salt: salt.to_vec(),
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,
enc_date_of_birth,
enc_default_user_profile_id,
})
}
fn decrypt(
&self,
file: &PassportFile,
password: &str,
) -> Result<(Seed, PublicKey, PrivateKey, Vec<UserProfile>, Option<DateOfBirth>, Option<String>), Self::Error> {
// Validate file format
validate_file_format(&file.kdf, &file.cipher)?;
// Derive KEK from password
let hk = Hkdf::<Sha256>::new(Some(&file.salt), password.as_bytes());
let mut kek = [0u8; 32];
hk.expand(KDF_INFO, &mut kek)
.map_err(|e| DomainError::CryptographicError(format!("HKDF failed: {}", e)))?;
// Decrypt seed
let cipher = XChaCha20Poly1305::new(&Key::from(kek));
let nonce = XNonce::from_slice(&file.nonce);
let seed_bytes = cipher
.decrypt(&nonce, &*file.enc_seed)
.map_err(|e| DomainError::CryptographicError(format!("Decryption failed: {}", e)))?;
let seed = Seed::new(seed_bytes);
// Re-derive keys from seed to verify
let key_deriver = Ed25519KeyDeriver;
let (public_key, private_key) = key_deriver.derive_from_seed(&seed)?;
// Verify public key matches
if public_key.0 != file.public_key {
return Err(DomainError::CryptographicError(
"Public key mismatch - wrong password?".to_string(),
));
}
// 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)))?;
// Decrypt date of birth
let date_of_birth_bytes = cipher
.decrypt(&nonce, &*file.enc_date_of_birth)
.map_err(|e| DomainError::CryptographicError(format!("Date of birth decryption failed: {}", e)))?;
let date_of_birth: Option<DateOfBirth> = serde_cbor::from_slice(&date_of_birth_bytes)
.map_err(|e| DomainError::CryptographicError(format!("Failed to deserialize date of birth: {}", e)))?;
// Decrypt default user profile ID
let default_user_profile_id_bytes = cipher
.decrypt(&nonce, &*file.enc_default_user_profile_id)
.map_err(|e| DomainError::CryptographicError(format!("Default user profile ID decryption failed: {}", e)))?;
let default_user_profile_id: Option<String> = serde_cbor::from_slice(&default_user_profile_id_bytes)
.map_err(|e| DomainError::CryptographicError(format!("Failed to deserialize default user profile ID: {}", e)))?;
// Note: univ_id is stored in the PassportFile and will be used when creating the Passport
Ok((seed, public_key, private_key, user_profiles, date_of_birth, default_user_profile_id))
}
}

View file

@ -0,0 +1,83 @@
//! Native-specific cryptographic tests
#[cfg(test)]
mod tests {
use crate::domain::entities::*;
use crate::domain::traits::{MnemonicGenerator, KeyDeriver, FileEncryptor};
use crate::infrastructure::crypto::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor};
#[test]
fn test_native_bip39_generator_creates_valid_mnemonic() {
let generator = Bip39MnemonicGenerator;
let phrase = generator.generate().unwrap();
// Should have 24 words
assert_eq!(phrase.words().len(), 24);
// Should be valid BIP-39
generator.validate(phrase.words()).unwrap();
}
#[test]
fn test_native_key_deriver_creates_consistent_keys() {
let deriver = Ed25519KeyDeriver;
// Create a test seed
let test_seed = Seed::new(vec![1; 32]);
let (public_key, private_key) = deriver.derive_from_seed(&test_seed).unwrap();
// Public key should be 32 bytes
assert_eq!(public_key.0.len(), 32);
// Private key should be 32 bytes
assert_eq!(private_key.0.len(), 32);
}
#[test]
fn test_native_file_encryptor_round_trip() {
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
let encrypted_file = encryptor.encrypt(&seed, password, &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[], &None, &None).unwrap();
// Verify file structure
assert_eq!(encrypted_file.kdf, "HKDF-SHA256");
assert_eq!(encrypted_file.cipher, "XChaCha20-Poly1305");
assert_eq!(encrypted_file.salt.len(), 32);
assert_eq!(encrypted_file.nonce.len(), 24);
assert_eq!(encrypted_file.public_key, public_key.0);
assert_eq!(encrypted_file.did, did.0);
// Decrypt
let (decrypted_seed, decrypted_public_key, _, _, _, _) = 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);
}
#[test]
fn test_native_file_encryptor_wrong_password_fails() {
let encryptor = XChaCha20FileEncryptor;
let seed = Seed::new(vec![1, 2, 3, 4, 5]);
let public_key = PublicKey(vec![1; 32]);
let did = Did::new(&public_key);
// Encrypt with one password
let encrypted_file = encryptor.encrypt(&seed, "correct-password", &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[], &None, &None).unwrap();
// Try to decrypt with wrong password
let result = encryptor.decrypt(&encrypted_file, "wrong-password");
// Should fail
assert!(result.is_err());
}
}

View file

@ -0,0 +1,35 @@
//! Shared cryptographic utilities and constants
/// Cryptographic constants used across implementations
pub const SEED_LENGTH: usize = 32;
pub const PUBLIC_KEY_LENGTH: usize = 32;
pub const PRIVATE_KEY_LENGTH: usize = 32;
pub const SALT_LENGTH: usize = 32;
pub const NONCE_LENGTH: usize = 24;
/// KDF and cipher identifiers
pub const KDF_HKDF_SHA256: &str = "HKDF-SHA256";
pub const CIPHER_XCHACHA20_POLY1305: &str = "XChaCha20-Poly1305";
/// HKDF info strings
pub const KDF_INFO: &[u8] = b"sharenet-passport-kek";
/// Helper function to validate seed length
pub fn validate_seed_length(seed_bytes: &[u8]) -> Result<(), crate::domain::error::DomainError> {
if seed_bytes.len() != SEED_LENGTH {
return Err(crate::domain::error::DomainError::CryptographicError(
format!("Invalid seed length: expected {}, got {}", SEED_LENGTH, seed_bytes.len())
));
}
Ok(())
}
/// Helper function to validate file format
pub fn validate_file_format(kdf: &str, cipher: &str) -> Result<(), crate::domain::error::DomainError> {
if kdf != KDF_HKDF_SHA256 || cipher != CIPHER_XCHACHA20_POLY1305 {
return Err(crate::domain::error::DomainError::InvalidFileFormat(
"Unsupported KDF or cipher".to_string()
));
}
Ok(())
}

View file

@ -0,0 +1,217 @@
//! WASM-compatible cryptographic implementations
use bip39::Mnemonic;
use ed25519_dalek::{SigningKey, SECRET_KEY_LENGTH};
use chacha20poly1305::{aead::{Aead, KeyInit}, XChaCha20Poly1305, Key, XNonce};
use hkdf::Hkdf;
use sha2::Sha256;
use crate::domain::entities::*;
use crate::domain::error::DomainError;
use crate::domain::traits::*;
use crate::infrastructure::rng;
use crate::infrastructure::time;
use super::shared::*;
#[derive(Clone)]
pub struct Bip39MnemonicGenerator;
impl MnemonicGenerator for Bip39MnemonicGenerator {
type Error = DomainError;
fn generate(&self) -> Result<RecoveryPhrase, Self::Error> {
let mut entropy = [0u8; 32];
let mut rng = rng::new_rng();
rng.fill_bytes(&mut entropy)?;
let mnemonic = Mnemonic::from_entropy(&entropy)
.map_err(|e| DomainError::CryptographicError(format!("Failed to generate mnemonic: {}", e)))?;
let words: Vec<String> = mnemonic.words().into_iter().map(|s| s.to_string()).collect();
Ok(RecoveryPhrase::new(words))
}
fn validate(&self, words: &[String]) -> Result<(), Self::Error> {
let phrase = words.join(" ");
Mnemonic::parse(&phrase)
.map_err(|e| DomainError::InvalidMnemonic(format!("Invalid mnemonic: {}", e)))?;
Ok(())
}
}
#[derive(Clone)]
pub struct Ed25519KeyDeriver;
impl KeyDeriver for Ed25519KeyDeriver {
type Error = DomainError;
fn derive_from_seed(&self, seed: &Seed) -> Result<(PublicKey, PrivateKey), Self::Error> {
validate_seed_length(seed.as_bytes())?;
let signing_key = SigningKey::from_bytes(&seed.as_bytes()[..SECRET_KEY_LENGTH].try_into()
.map_err(|_| DomainError::CryptographicError("Invalid seed length".to_string()))?);
let verifying_key = signing_key.verifying_key();
Ok((
PublicKey(verifying_key.to_bytes().to_vec()),
PrivateKey(signing_key.to_bytes().to_vec()),
))
}
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 univ_id as passphrase to bind seed to universe
let bip39_seed = bip39_mnemonic.to_seed(univ_id);
// BIP39 produces 64-byte seed, but we only need 32 bytes for Ed25519
// Use the first 32 bytes of the BIP39 seed
let ed25519_seed: [u8; 32] = bip39_seed[..32]
.try_into()
.map_err(|_| DomainError::CryptographicError("Failed to extract 32-byte seed from BIP39 seed".to_string()))?;
Ok(Seed::new(ed25519_seed.to_vec()))
}
}
#[derive(Clone)]
pub struct XChaCha20FileEncryptor;
impl FileEncryptor for XChaCha20FileEncryptor {
type Error = DomainError;
fn encrypt(
&self,
seed: &Seed,
password: &str,
public_key: &PublicKey,
did: &Did,
univ_id: &str,
user_profiles: &[UserProfile],
date_of_birth: &Option<DateOfBirth>,
default_user_profile_id: &Option<String>,
) -> Result<PassportFile, Self::Error> {
// Generate salt and nonce using WASM-compatible RNG
let mut salt = [0u8; SALT_LENGTH];
let mut nonce_bytes = [0u8; NONCE_LENGTH];
let mut rng = rng::new_rng();
rng.fill_bytes(&mut salt)?;
rng.fill_bytes(&mut nonce_bytes)?;
// Derive KEK from password using HKDF
let hk = Hkdf::<Sha256>::new(Some(&salt), password.as_bytes());
let mut kek = [0u8; 32];
hk.expand(KDF_INFO, &mut kek)
.map_err(|e| DomainError::CryptographicError(format!("HKDF failed: {}", e)))?;
// Encrypt seed
let cipher = XChaCha20Poly1305::new(&Key::from(kek));
let nonce = XNonce::from_slice(&nonce_bytes);
let enc_seed = cipher
.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)))?;
// Serialize and encrypt date of birth
let date_of_birth_bytes = serde_cbor::to_vec(&date_of_birth)
.map_err(|e| DomainError::CryptographicError(format!("Failed to serialize date of birth: {}", e)))?;
let enc_date_of_birth = cipher
.encrypt(&nonce, &*date_of_birth_bytes)
.map_err(|e| DomainError::CryptographicError(format!("Date of birth encryption failed: {}", e)))?;
// Serialize and encrypt default user profile ID
let default_user_profile_id_bytes = serde_cbor::to_vec(&default_user_profile_id)
.map_err(|e| DomainError::CryptographicError(format!("Failed to serialize default user profile ID: {}", e)))?;
let enc_default_user_profile_id = cipher
.encrypt(&nonce, &*default_user_profile_id_bytes)
.map_err(|e| DomainError::CryptographicError(format!("Default user profile ID encryption failed: {}", e)))?;
// Get current timestamp using WASM-compatible time
let created_at = time::now_seconds()?;
Ok(PassportFile {
enc_seed,
kdf: KDF_HKDF_SHA256.to_string(),
cipher: CIPHER_XCHACHA20_POLY1305.to_string(),
salt: salt.to_vec(),
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,
enc_date_of_birth,
enc_default_user_profile_id,
})
}
fn decrypt(
&self,
file: &PassportFile,
password: &str,
) -> Result<(Seed, PublicKey, PrivateKey, Vec<UserProfile>, Option<DateOfBirth>, Option<String>), Self::Error> {
// Validate file format
validate_file_format(&file.kdf, &file.cipher)?;
// Derive KEK from password
let hk = Hkdf::<Sha256>::new(Some(&file.salt), password.as_bytes());
let mut kek = [0u8; 32];
hk.expand(KDF_INFO, &mut kek)
.map_err(|e| DomainError::CryptographicError(format!("HKDF failed: {}", e)))?;
// Decrypt seed
let cipher = XChaCha20Poly1305::new(&Key::from(kek));
let nonce = XNonce::from_slice(&file.nonce);
let seed_bytes = cipher
.decrypt(&nonce, &*file.enc_seed)
.map_err(|e| DomainError::CryptographicError(format!("Decryption failed: {}", e)))?;
let seed = Seed::new(seed_bytes);
// Re-derive keys from seed to verify
let key_deriver = Ed25519KeyDeriver;
let (public_key, private_key) = key_deriver.derive_from_seed(&seed)?;
// Verify public key matches
if public_key.0 != file.public_key {
return Err(DomainError::CryptographicError(
"Public key mismatch - wrong password?".to_string(),
));
}
// 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)))?;
// Decrypt date of birth
let date_of_birth_bytes = cipher
.decrypt(&nonce, &*file.enc_date_of_birth)
.map_err(|e| DomainError::CryptographicError(format!("Date of birth decryption failed: {}", e)))?;
let date_of_birth: Option<DateOfBirth> = serde_cbor::from_slice(&date_of_birth_bytes)
.map_err(|e| DomainError::CryptographicError(format!("Failed to deserialize date of birth: {}", e)))?;
// Decrypt default user profile ID
let default_user_profile_id_bytes = cipher
.decrypt(&nonce, &*file.enc_default_user_profile_id)
.map_err(|e| DomainError::CryptographicError(format!("Default user profile ID decryption failed: {}", e)))?;
let default_user_profile_id: Option<String> = serde_cbor::from_slice(&default_user_profile_id_bytes)
.map_err(|e| DomainError::CryptographicError(format!("Failed to deserialize default user profile ID: {}", e)))?;
// Note: univ_id is stored in the PassportFile and will be used when creating the Passport
Ok((seed, public_key, private_key, user_profiles, date_of_birth, default_user_profile_id))
}
}

View file

@ -0,0 +1,83 @@
//! WASM-specific cryptographic tests
#[cfg(test)]
mod tests {
use crate::domain::entities::*;
use crate::domain::traits::{MnemonicGenerator, KeyDeriver, FileEncryptor};
use crate::infrastructure::crypto::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor};
#[test]
fn test_wasm_bip39_generator_creates_valid_mnemonic() {
let generator = Bip39MnemonicGenerator;
let phrase = generator.generate().unwrap();
// Should have 24 words
assert_eq!(phrase.words().len(), 24);
// Should be valid BIP-39
generator.validate(phrase.words()).unwrap();
}
#[test]
fn test_wasm_key_deriver_creates_consistent_keys() {
let deriver = Ed25519KeyDeriver;
// Create a test seed
let test_seed = Seed::new(vec![1; 32]);
let (public_key, private_key) = deriver.derive_from_seed(&test_seed).unwrap();
// Public key should be 32 bytes
assert_eq!(public_key.0.len(), 32);
// Private key should be 32 bytes
assert_eq!(private_key.0.len(), 32);
}
#[test]
fn test_wasm_file_encryptor_round_trip() {
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
let encrypted_file = encryptor.encrypt(&seed, password, &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[], &None, &None).unwrap();
// Verify file structure
assert_eq!(encrypted_file.kdf, "HKDF-SHA256");
assert_eq!(encrypted_file.cipher, "XChaCha20-Poly1305");
assert_eq!(encrypted_file.salt.len(), 32);
assert_eq!(encrypted_file.nonce.len(), 24);
assert_eq!(encrypted_file.public_key, public_key.0);
assert_eq!(encrypted_file.did, did.0);
// Decrypt
let (decrypted_seed, decrypted_public_key, _, _, _, _) = 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);
}
#[test]
fn test_wasm_file_encryptor_wrong_password_fails() {
let encryptor = XChaCha20FileEncryptor;
let seed = Seed::new(vec![1, 2, 3, 4, 5]);
let public_key = PublicKey(vec![1; 32]);
let did = Did::new(&public_key);
// Encrypt with one password
let encrypted_file = encryptor.encrypt(&seed, "correct-password", &public_key, &did, "u:Test Universe:12345678-1234-1234-1234-123456789012", &[], &None, &None).unwrap();
// Try to decrypt with wrong password
let result = encryptor.decrypt(&encrypted_file, "wrong-password");
// Should fail
assert!(result.is_err());
}
}

View file

@ -0,0 +1,14 @@
// Core abstractions for all platforms
pub mod traits;
pub mod rng;
pub mod time;
pub mod crypto;
pub mod storage;
// Export platform-appropriate implementations
pub use crypto::*;
pub use storage::*;
// Re-export traits for convenience
pub use traits::*;

View file

@ -0,0 +1,49 @@
//! Random number generation abstraction for WASM compatibility
use crate::domain::error::DomainError;
/// Random number generator trait
pub trait RngCore {
/// Fill a buffer with random bytes
fn fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), DomainError>;
}
/// Standard library RNG using OsRng
#[cfg(not(target_arch = "wasm32"))]
pub struct StdRng;
#[cfg(not(target_arch = "wasm32"))]
impl RngCore for StdRng {
fn fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), DomainError> {
use rand::{rngs::OsRng, RngCore};
OsRng
.try_fill_bytes(dest)
.map_err(|e| DomainError::CryptographicError(format!("RNG error: {}", e)))
}
}
/// WASM-compatible RNG using getrandom
#[cfg(target_arch = "wasm32")]
pub struct WasmRng;
#[cfg(target_arch = "wasm32")]
impl RngCore for WasmRng {
fn fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), DomainError> {
getrandom::getrandom(dest)
.map_err(|e| DomainError::CryptographicError(format!("WASM RNG error: {}", e)))
}
}
/// Create a new RNG instance based on the current architecture
pub fn new_rng() -> Box<dyn RngCore> {
#[cfg(not(target_arch = "wasm32"))]
{
Box::new(StdRng)
}
#[cfg(target_arch = "wasm32")]
{
Box::new(WasmRng)
}
}

View file

@ -0,0 +1,22 @@
//! Unified storage API with target-specific implementations
// Platform-specific implementations
#[cfg(all(not(target_arch = "wasm32"), not(feature = "force-wasm")))]
mod native;
#[cfg(any(target_arch = "wasm32", feature = "force-wasm"))]
mod wasm;
// Re-export the unified API
#[cfg(all(not(target_arch = "wasm32"), not(feature = "force-wasm")))]
pub use native::FileSystemStorage;
#[cfg(any(target_arch = "wasm32", feature = "force-wasm"))]
pub use wasm::BrowserStorage as FileSystemStorage;
// Target-specific tests
#[cfg(all(test, all(not(target_arch = "wasm32"), not(feature = "force-wasm"))))]
mod native_test;
#[cfg(all(test, any(target_arch = "wasm32", feature = "force-wasm")))]
mod wasm_test;

View file

@ -0,0 +1,58 @@
//! Native (std) file system storage implementation
use std::fs;
use std::path::Path;
use crate::domain::entities::*;
use crate::domain::error::DomainError;
use crate::domain::traits::*;
/// Native file system storage
#[derive(Clone)]
pub struct FileSystemStorage;
impl FileStorage for FileSystemStorage {
type Error = DomainError;
fn save(&self, file: &PassportFile, path: &str) -> Result<(), Self::Error> {
let path = Path::new(path);
// Create parent directories if they don't exist
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| DomainError::InvalidFileFormat(format!("Failed to create directories: {}", e)))?;
}
// Serialize to CBOR
let data = serde_cbor::to_vec(file)
.map_err(|e| DomainError::InvalidFileFormat(format!("Failed to serialize file: {}", e)))?;
// Write file
fs::write(path, data)
.map_err(|e| DomainError::InvalidFileFormat(format!("Failed to write file: {}", e)))?;
// Set secure permissions (Unix-like systems)
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(path)
.map_err(|e| DomainError::InvalidFileFormat(format!("Failed to get file metadata: {}", e)))?
.permissions();
perms.set_mode(0o600); // rw-------
fs::set_permissions(path, perms)
.map_err(|e| DomainError::InvalidFileFormat(format!("Failed to set permissions: {}", e)))?;
}
Ok(())
}
fn load(&self, path: &str) -> Result<PassportFile, Self::Error> {
let data = fs::read(path)
.map_err(|e| DomainError::InvalidFileFormat(format!("Failed to read file: {}", e)))?;
let file = serde_cbor::from_slice(&data)
.map_err(|e| DomainError::InvalidFileFormat(format!("Failed to deserialize file: {}", e)))?;
Ok(file)
}
}

View file

@ -0,0 +1,60 @@
//! Native-specific storage tests
#[cfg(test)]
mod tests {
use crate::domain::entities::*;
use crate::domain::traits::FileStorage;
use crate::infrastructure::storage::FileSystemStorage;
#[test]
fn test_native_storage_save_and_load() {
let storage = FileSystemStorage;
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test-passport.spf");
// Create a test passport file
let test_file = PassportFile {
kdf: "HKDF-SHA256".to_string(),
cipher: "XChaCha20-Poly1305".to_string(),
salt: vec![1; 32],
nonce: vec![2; 24],
enc_seed: vec![3; 32],
public_key: vec![4; 32],
did: "did:sharenet:test".to_string(),
univ_id: "u:Test Universe:12345678-1234-1234-1234-123456789012".to_string(),
created_at: 1234567890,
version: "1.0.0".to_string(),
enc_user_profiles: vec![],
enc_date_of_birth: vec![],
enc_default_user_profile_id: vec![],
};
// Save the file
storage.save(&test_file, file_path.to_str().unwrap()).unwrap();
// Load the file
let loaded_file = storage.load(file_path.to_str().unwrap()).unwrap();
// Verify the loaded file matches the original
assert_eq!(loaded_file.kdf, test_file.kdf);
assert_eq!(loaded_file.cipher, test_file.cipher);
assert_eq!(loaded_file.salt, test_file.salt);
assert_eq!(loaded_file.nonce, test_file.nonce);
assert_eq!(loaded_file.enc_seed, test_file.enc_seed);
assert_eq!(loaded_file.public_key, test_file.public_key);
assert_eq!(loaded_file.did, test_file.did);
assert_eq!(loaded_file.univ_id, test_file.univ_id);
assert_eq!(loaded_file.created_at, test_file.created_at);
assert_eq!(loaded_file.version, test_file.version);
assert_eq!(loaded_file.enc_user_profiles, test_file.enc_user_profiles);
}
#[test]
fn test_native_storage_load_nonexistent_file_fails() {
let storage = FileSystemStorage;
let result = storage.load("/nonexistent/path/file.spf");
// Should fail
assert!(result.is_err());
}
}

View file

@ -0,0 +1,110 @@
//! WASM-compatible storage using browser LocalStorage
use crate::domain::entities::*;
use crate::domain::error::DomainError;
use crate::domain::traits::*;
/// WASM storage using browser LocalStorage
#[derive(Clone)]
pub struct BrowserStorage;
// Mock storage for testing on native targets
#[cfg(not(target_arch = "wasm32"))]
use std::collections::HashMap;
#[cfg(not(target_arch = "wasm32"))]
use std::sync::{Mutex, OnceLock};
#[cfg(not(target_arch = "wasm32"))]
static MOCK_STORAGE: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
impl FileStorage for BrowserStorage {
type Error = DomainError;
fn save(&self, file: &PassportFile, path: &str) -> Result<(), Self::Error> {
// Real implementation for WASM targets
#[cfg(target_arch = "wasm32")]
{
use base64::Engine;
use gloo_storage::Storage;
// Serialize to CBOR
let data = serde_cbor::to_vec(file)
.map_err(|e| DomainError::InvalidFileFormat(format!("Failed to serialize file: {}", e)))?;
// Convert to base64 for storage
let base64_data = base64::engine::general_purpose::STANDARD.encode(&data);
// Store in browser localStorage using gloo-storage (synchronous)
gloo_storage::LocalStorage::set(path, base64_data)
.map_err(|e| DomainError::InvalidFileFormat(format!("Failed to store in localStorage: {}", e)))?;
Ok(())
}
// Mock implementation for testing on native targets
#[cfg(not(target_arch = "wasm32"))]
{
// For testing purposes, we'll use a simple in-memory storage
// In a real browser environment, this would use actual localStorage
use base64::Engine;
// Serialize to CBOR
let data = serde_cbor::to_vec(file)
.map_err(|e| DomainError::InvalidFileFormat(format!("Failed to serialize file: {}", e)))?;
// Convert to base64 for storage
let base64_data = base64::engine::general_purpose::STANDARD.encode(&data);
// Store in mock storage
let mock_storage = MOCK_STORAGE.get_or_init(|| Mutex::new(HashMap::new()));
mock_storage.lock().unwrap().insert(path.to_string(), base64_data);
Ok(())
}
}
fn load(&self, path: &str) -> Result<PassportFile, Self::Error> {
// Real implementation for WASM targets
#[cfg(target_arch = "wasm32")]
{
use base64::Engine;
use gloo_storage::Storage;
// Load from browser localStorage (synchronous)
let base64_data: String = gloo_storage::LocalStorage::get(path)
.map_err(|e| DomainError::InvalidFileFormat(format!("Failed to load from localStorage: {}", e)))?;
// Decode from base64
let data = base64::engine::general_purpose::STANDARD.decode(&base64_data)
.map_err(|e| DomainError::InvalidFileFormat(format!("Failed to decode base64: {}", e)))?;
// Deserialize from CBOR
let file = serde_cbor::from_slice(&data)
.map_err(|e| DomainError::InvalidFileFormat(format!("Failed to deserialize file: {}", e)))?;
Ok(file)
}
// Mock implementation for testing on native targets
#[cfg(not(target_arch = "wasm32"))]
{
use base64::Engine;
// Load from mock storage
let mock_storage = MOCK_STORAGE.get_or_init(|| Mutex::new(HashMap::new()));
let mock_storage = mock_storage.lock().unwrap();
let base64_data = mock_storage.get(path)
.ok_or_else(|| DomainError::InvalidFileFormat(format!("Key not found: {}", path)))?;
// Decode from base64
let data = base64::engine::general_purpose::STANDARD.decode(base64_data)
.map_err(|e| DomainError::InvalidFileFormat(format!("Failed to decode base64: {}", e)))?;
// Deserialize from CBOR
let file = serde_cbor::from_slice(&data)
.map_err(|e| DomainError::InvalidFileFormat(format!("Failed to deserialize file: {}", e)))?;
Ok(file)
}
}
}

View file

@ -0,0 +1,59 @@
//! WASM-specific storage tests
#[cfg(test)]
mod tests {
use crate::domain::entities::*;
use crate::domain::traits::FileStorage;
use crate::infrastructure::storage::FileSystemStorage;
#[test]
fn test_wasm_storage_save_and_load() {
let storage = FileSystemStorage;
let test_key = "test-passport-key";
// Create a test passport file
let test_file = PassportFile {
kdf: "HKDF-SHA256".to_string(),
cipher: "XChaCha20-Poly1305".to_string(),
salt: vec![1; 32],
nonce: vec![2; 24],
enc_seed: vec![3; 32],
public_key: vec![4; 32],
did: "did:sharenet:test".to_string(),
univ_id: "u:Test Universe:12345678-1234-1234-1234-123456789012".to_string(),
created_at: 1234567890,
version: "1.0.0".to_string(),
enc_user_profiles: vec![],
enc_date_of_birth: vec![],
enc_default_user_profile_id: vec![],
};
// Save the file
storage.save(&test_file, test_key).unwrap();
// Load the file
let loaded_file = storage.load(test_key).unwrap();
// Verify the loaded file matches the original
assert_eq!(loaded_file.kdf, test_file.kdf);
assert_eq!(loaded_file.cipher, test_file.cipher);
assert_eq!(loaded_file.salt, test_file.salt);
assert_eq!(loaded_file.nonce, test_file.nonce);
assert_eq!(loaded_file.enc_seed, test_file.enc_seed);
assert_eq!(loaded_file.public_key, test_file.public_key);
assert_eq!(loaded_file.did, test_file.did);
assert_eq!(loaded_file.univ_id, test_file.univ_id);
assert_eq!(loaded_file.created_at, test_file.created_at);
assert_eq!(loaded_file.version, test_file.version);
assert_eq!(loaded_file.enc_user_profiles, test_file.enc_user_profiles);
}
#[test]
fn test_wasm_storage_load_nonexistent_key_fails() {
let storage = FileSystemStorage;
let result = storage.load("nonexistent-key");
// Should fail
assert!(result.is_err());
}
}

View file

@ -0,0 +1,52 @@
//! Time abstraction for WASM compatibility
use crate::domain::error::DomainError;
/// Time provider trait for abstracting time operations
pub trait TimeProvider {
/// Get current timestamp in seconds since Unix epoch
fn now_seconds() -> Result<u64, DomainError>;
}
/// Standard library time provider
#[cfg(not(target_arch = "wasm32"))]
pub struct StdTimeProvider;
#[cfg(not(target_arch = "wasm32"))]
impl TimeProvider for StdTimeProvider {
fn now_seconds() -> Result<u64, DomainError> {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| DomainError::CryptographicError(format!("Time error: {}", e)))
.map(|d| d.as_secs())
}
}
/// WASM time provider
#[cfg(target_arch = "wasm32")]
pub struct WasmTimeProvider;
#[cfg(target_arch = "wasm32")]
impl TimeProvider for WasmTimeProvider {
fn now_seconds() -> Result<u64, DomainError> {
// Use JavaScript Date API via js_sys
// This will work when compiled to WASM
let timestamp = js_sys::Date::now() / 1000.0; // Convert from milliseconds to seconds
Ok(timestamp as u64)
}
}
/// Get the current timestamp using the appropriate time provider
pub fn now_seconds() -> Result<u64, DomainError> {
#[cfg(not(target_arch = "wasm32"))]
{
StdTimeProvider::now_seconds()
}
#[cfg(target_arch = "wasm32")]
{
WasmTimeProvider::now_seconds()
}
}

View file

@ -0,0 +1,65 @@
//! Core abstractions for platform-agnostic cryptography and storage
use crate::domain::entities::*;
use crate::domain::error::DomainError;
/// Mnemonic generation trait
pub trait MnemonicGenerator {
type Error: Into<DomainError>;
fn generate(&self) -> Result<RecoveryPhrase, Self::Error>;
fn validate(&self, words: &[String]) -> Result<(), Self::Error>;
}
/// Key derivation trait
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, univ_id: &str) -> Result<Seed, Self::Error>;
}
/// File encryption trait
pub trait FileEncryptor {
type Error: Into<DomainError>;
fn encrypt(
&self,
seed: &Seed,
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, Vec<UserProfile>), Self::Error>;
}
/// Storage trait for passport files
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait(?Send))]
pub trait FileStorage {
type Error: Into<DomainError>;
async fn save(&self, file: &PassportFile, path: &str) -> Result<(), Self::Error>;
async fn load(&self, path: &str) -> Result<PassportFile, Self::Error>;
}
/// Random number generation trait
pub trait RngCore {
type Error: Into<DomainError>;
fn fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error>;
}
/// Time provider trait
pub trait TimeProvider {
type Error: Into<DomainError>;
fn now_seconds(&self) -> Result<u64, Self::Error>;
}

55
passport/src/lib.rs Normal file
View file

@ -0,0 +1,55 @@
//! Sharenet Passport Core Library
//!
//! This library provides core functionality for creating, managing, and verifying
//! Sharenet Passports using the .spf file format.
pub mod domain;
pub mod application;
pub mod infrastructure;
#[cfg(any(target_arch = "wasm32", feature = "force-wasm"))]
pub mod wasm;
// Re-export WASM API functions when building for WASM target
#[cfg(any(target_arch = "wasm32", feature = "force-wasm"))]
pub use wasm::{
create_passport,
import_from_recovery,
import_from_encrypted_data,
export_to_encrypted_data,
sign_message,
generate_recovery_phrase,
validate_recovery_phrase,
create_user_profile,
update_user_profile,
delete_user_profile,
change_passport_password,
get_passport_metadata,
validate_passport_file,
};
#[cfg(any(target_arch = "wasm32", feature = "force-wasm"))]
#[cfg(test)]
pub mod wasm_test;
// Public API surface
pub use domain::entities::{Passport, RecoveryPhrase, PassportFile, PublicKey, PrivateKey, Did, Seed};
pub use domain::traits::{MnemonicGenerator, KeyDeriver, FileEncryptor, FileStorage};
pub use domain::error::DomainError;
pub use application::use_cases::{
CreatePassportUseCase,
ImportFromRecoveryUseCase,
ImportFromFileUseCase,
ExportPassportUseCase,
SignCardUseCase
};
pub use application::error::ApplicationError;
// Re-export infrastructure implementations (automatically selected by target)
pub use infrastructure::{
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage,
};

View 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");
}
}

346
passport/src/wasm.rs Normal file
View file

@ -0,0 +1,346 @@
//! Browser-specific WASM API for Sharenet Passport
//!
//! This module provides browser-compatible functions that work with in-memory data
//! and return encrypted data as bytes. The library is purely in-memory and does not
//! handle any I/O operations - the consumer must handle storage/retrieval.
use wasm_bindgen::prelude::*;
use crate::application::use_cases::{
SignCardUseCase,
};
use crate::infrastructure::{
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
};
use crate::domain::entities::{Passport, UserIdentity, UserPreferences, PassportFile, RecoveryPhrase, UserProfile, Did};
use crate::domain::traits::{MnemonicGenerator, KeyDeriver, FileEncryptor};
/// Create a new passport with the given universe ID and password
///
/// Returns a JSON string containing both the passport and recovery phrase
/// This function works entirely in memory and doesn't write to any storage.
#[wasm_bindgen]
pub fn create_passport(
univ_id: String,
_password: String,
) -> Result<JsValue, JsValue> {
// For WASM, we need to create a passport in memory without file operations
// This is a simplified version that creates the passport structure directly
let generator = Bip39MnemonicGenerator;
let key_deriver = Ed25519KeyDeriver;
match generator.generate() {
Ok(recovery_phrase) => {
match key_deriver.derive_from_mnemonic(&recovery_phrase, &univ_id) {
Ok(seed) => {
// Derive keys from seed
let (public_key, private_key) = key_deriver.derive_from_seed(&seed)
.map_err(|e| JsValue::from_str(&format!("Error deriving keys from seed: {}", e)))?;
// Create passport with default user profile
let passport = Passport::new(
seed,
public_key,
private_key,
univ_id,
);
let result = serde_wasm_bindgen::to_value(&serde_json::json!({
"passport": passport,
"recovery_phrase": recovery_phrase
})).map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
Ok(result)
}
Err(e) => Err(JsValue::from_str(&format!("Error deriving keys: {}", e))),
}
}
Err(e) => Err(JsValue::from_str(&format!("Error generating recovery phrase: {}", e))),
}
}
/// Import a passport from recovery phrase
/// Returns the imported passport as JSON
#[wasm_bindgen]
pub fn import_from_recovery(
univ_id: String,
recovery_words: Vec<String>,
_password: String,
) -> Result<JsValue, JsValue> {
let generator = Bip39MnemonicGenerator;
let key_deriver = Ed25519KeyDeriver;
// Validate recovery phrase
if let Err(_) = generator.validate(&recovery_words) {
return Err(JsValue::from_str("Invalid recovery phrase"));
}
// Reconstruct recovery phrase from words
let recovery_phrase = RecoveryPhrase::new(recovery_words);
// Derive keys from recovery phrase
match key_deriver.derive_from_mnemonic(&recovery_phrase, &univ_id) {
Ok(seed) => {
// Derive keys from seed
let (public_key, private_key) = key_deriver.derive_from_seed(&seed)
.map_err(|e| JsValue::from_str(&format!("Error deriving keys from seed: {}", e)))?;
// Create passport with default user profile
let passport = Passport::new(
seed,
public_key,
private_key,
univ_id,
);
let result = serde_wasm_bindgen::to_value(&passport)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
Ok(result)
}
Err(e) => Err(JsValue::from_str(&format!("Error deriving keys: {}", e))),
}
}
/// Load a passport from encrypted data (ArrayBuffer/Blob)
/// This accepts encrypted passport data as bytes and returns the decrypted passport
#[wasm_bindgen]
pub fn import_from_encrypted_data(
encrypted_data: Vec<u8>,
password: String,
) -> Result<JsValue, JsValue> {
// Deserialize the encrypted passport file
let passport_file: PassportFile = serde_cbor::from_slice(&encrypted_data)
.map_err(|e| JsValue::from_str(&format!("Failed to deserialize passport file: {}", e)))?;
// Decrypt the passport file using the password
let encryptor = XChaCha20FileEncryptor;
let (seed, public_key, private_key, user_profiles, date_of_birth, default_user_profile_id) = encryptor.decrypt(
&passport_file,
&password,
).map_err(|e| JsValue::from_str(&format!("Failed to decrypt passport: {}", e)))?;
// Create passport with decrypted user profiles instead of creating a new default one
let did = Did::new(&public_key);
let passport = Passport {
seed,
public_key,
private_key,
did,
univ_id: passport_file.univ_id,
user_profiles,
date_of_birth,
default_user_profile_id,
};
let result = serde_wasm_bindgen::to_value(&passport)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
Ok(result)
}
/// Export a passport to encrypted data (ArrayBuffer/Blob)
/// This returns encrypted passport data as bytes that can be downloaded or stored
#[wasm_bindgen]
pub fn export_to_encrypted_data(
passport_json: JsValue,
password: String,
) -> Result<Vec<u8>, JsValue> {
let passport: Passport = serde_wasm_bindgen::from_value(passport_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
let encryptor = XChaCha20FileEncryptor;
// Encrypt the passport data
let passport_file = encryptor.encrypt(
&passport.seed,
&password,
&passport.public_key,
&passport.did,
&passport.univ_id,
&passport.user_profiles,
&passport.date_of_birth,
&passport.default_user_profile_id,
).map_err(|e| JsValue::from_str(&format!("Failed to encrypt passport: {}", e)))?;
// Serialize to bytes for browser download
serde_cbor::to_vec(&passport_file)
.map_err(|e| JsValue::from_str(&format!("Failed to serialize passport file: {}", e)))
}
/// Sign a message with the passport's private key
#[wasm_bindgen]
pub fn sign_message(
passport_json: JsValue,
message: String,
) -> Result<Vec<u8>, JsValue> {
let passport: Passport = serde_wasm_bindgen::from_value(passport_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
let use_case = SignCardUseCase::new();
match use_case.execute(&passport, &message) {
Ok(signature) => Ok(signature),
Err(e) => Err(JsValue::from_str(&format!("Error signing message: {}", e))),
}
}
/// Generate a new recovery phrase
#[wasm_bindgen]
pub fn generate_recovery_phrase() -> Result<JsValue, JsValue> {
let generator = Bip39MnemonicGenerator;
match generator.generate() {
Ok(recovery_phrase) => {
let result = serde_wasm_bindgen::to_value(&recovery_phrase)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
Ok(result)
}
Err(e) => Err(JsValue::from_str(&format!("Error generating recovery phrase: {}", e))),
}
}
/// Validate a recovery phrase
#[wasm_bindgen]
pub fn validate_recovery_phrase(recovery_words: Vec<String>) -> Result<bool, JsValue> {
let generator = Bip39MnemonicGenerator;
match generator.validate(&recovery_words) {
Ok(()) => Ok(true),
Err(_) => Ok(false),
}
}
/// Create a new user profile for a passport
/// Returns the updated passport as JSON
#[wasm_bindgen]
pub fn create_user_profile(
passport_json: JsValue,
hub_did: Option<String>,
identity_json: JsValue,
preferences_json: JsValue,
) -> Result<JsValue, JsValue> {
let mut passport: Passport = serde_wasm_bindgen::from_value(passport_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
let identity: UserIdentity = serde_wasm_bindgen::from_value(identity_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
let preferences: UserPreferences = serde_wasm_bindgen::from_value(preferences_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
// Create new user profile and add to passport (in-memory operation)
let profile = UserProfile::new(hub_did, identity, preferences);
passport.add_user_profile(profile)
.map_err(|e| JsValue::from_str(&format!("Error adding user profile: {}", e)))?;
let result = serde_wasm_bindgen::to_value(&passport)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
Ok(result)
}
/// Update an existing user profile
/// Returns the updated passport as JSON
#[wasm_bindgen]
pub fn update_user_profile(
passport_json: JsValue,
profile_id: String,
identity_json: JsValue,
preferences_json: JsValue,
) -> Result<JsValue, JsValue> {
let mut passport: Passport = serde_wasm_bindgen::from_value(passport_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
let identity: UserIdentity = serde_wasm_bindgen::from_value(identity_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
let preferences: UserPreferences = serde_wasm_bindgen::from_value(preferences_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
// Update user profile directly in passport (in-memory operation)
let profile = UserProfile::new(None, identity, preferences);
passport.update_user_profile_by_id(&profile_id, profile)
.map_err(|e| JsValue::from_str(&format!("Error updating user profile: {}", e)))?;
let result = serde_wasm_bindgen::to_value(&passport)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
Ok(result)
}
/// Delete a user profile
/// Returns the updated passport as JSON
#[wasm_bindgen]
pub fn delete_user_profile(
passport_json: JsValue,
profile_id: String,
) -> Result<JsValue, JsValue> {
let mut passport: Passport = serde_wasm_bindgen::from_value(passport_json)
.map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))?;
// Delete user profile directly from passport (in-memory operation)
passport.remove_user_profile_by_id(&profile_id)
.map_err(|e| JsValue::from_str(&format!("Error deleting user profile: {}", e)))?;
let result = serde_wasm_bindgen::to_value(&passport)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
Ok(result)
}
/// Change passport password
/// Returns the updated passport as JSON
#[wasm_bindgen]
pub fn change_passport_password(
_passport_json: JsValue,
_old_password: String,
_new_password: String,
) -> Result<JsValue, JsValue> {
// Note: This function requires re-encryption which typically needs file operations
// In a browser environment, you'd need to handle this differently
// For now, we'll return an error indicating this operation isn't supported
Err(JsValue::from_str(
"Password change requires file operations which are not supported in browser environment. "
))
}
/// Get passport metadata from encrypted data
/// This can extract public metadata without full decryption
#[wasm_bindgen]
pub fn get_passport_metadata(
encrypted_data: Vec<u8>,
) -> Result<JsValue, JsValue> {
// Deserialize the encrypted passport file
let passport_file: PassportFile = serde_cbor::from_slice(&encrypted_data)
.map_err(|e| JsValue::from_str(&format!("Failed to deserialize passport file: {}", e)))?;
let metadata = serde_json::json!({
"did": passport_file.did,
"univ_id": passport_file.univ_id,
"public_key": hex::encode(&passport_file.public_key),
"created_at": passport_file.created_at,
"version": passport_file.version,
});
let result = serde_wasm_bindgen::to_value(&metadata)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))?;
Ok(result)
}
/// Validate passport file integrity from encrypted data
#[wasm_bindgen]
pub fn validate_passport_file(
encrypted_data: Vec<u8>,
) -> Result<bool, JsValue> {
match serde_cbor::from_slice::<PassportFile>(&encrypted_data) {
Ok(passport_file) => {
// Basic validation checks
let is_valid = !passport_file.enc_seed.is_empty()
&& !passport_file.salt.is_empty()
&& !passport_file.nonce.is_empty()
&& !passport_file.public_key.is_empty()
&& !passport_file.did.is_empty()
&& !passport_file.univ_id.is_empty();
Ok(is_valid)
}
Err(_) => Ok(false),
}
}