feature/3-add-passport-library-and-cli-into-project #4
44 changed files with 11224 additions and 35 deletions
64
frontend/wasm/Cargo.lock
generated
64
frontend/wasm/Cargo.lock
generated
|
|
@ -415,6 +415,36 @@ version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "passport"
|
||||||
|
version = "0.4.1"
|
||||||
|
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",
|
||||||
|
"thiserror",
|
||||||
|
"uuid",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-time",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkcs8"
|
name = "pkcs8"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
|
|
@ -601,47 +631,15 @@ dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sharenet-passport"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "sparse+https://git.sharenet.sh/api/packages/devteam/cargo/"
|
|
||||||
checksum = "bec9d785a802bbfcd6a84f72f2a53e50729847a68ed5f4e6ea1310177bfe4c43"
|
|
||||||
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",
|
|
||||||
"thiserror",
|
|
||||||
"uuid",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"web-time",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sharenet-passport-wasm"
|
name = "sharenet-passport-wasm"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.16",
|
"getrandom 0.2.16",
|
||||||
|
"passport",
|
||||||
"serde",
|
"serde",
|
||||||
"serde-wasm-bindgen",
|
"serde-wasm-bindgen",
|
||||||
"serde_cbor",
|
"serde_cbor",
|
||||||
"sharenet-passport",
|
|
||||||
"uuid",
|
"uuid",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ wasm-bindgen = "0.2.105"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde-wasm-bindgen = "0.6"
|
serde-wasm-bindgen = "0.6"
|
||||||
serde_cbor = "0.11"
|
serde_cbor = "0.11"
|
||||||
sharenet-passport = { version = "0.4.0", registry = "sharenet-sh-forgejo", features = ["force-wasm"] }
|
passport = { path = "../../passport", features = ["force-wasm"] }
|
||||||
|
|
||||||
# WASM-compatible random number generation
|
# WASM-compatible random number generation
|
||||||
getrandom = { version = "0.2", features = ["js"] }
|
getrandom = { version = "0.2", features = ["js"] }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
// Re-export all functions from the sharenet-passport crate
|
// Re-export all functions from the sharenet-passport crate
|
||||||
pub use sharenet_passport::wasm::*;
|
pub use passport::wasm::*;
|
||||||
1282
passport-cli/Cargo.lock
generated
Normal file
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
18
passport-cli/Cargo.toml
Normal 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"
|
||||||
71
passport-cli/src/bin/inspect_file.rs
Normal file
71
passport-cli/src/bin/inspect_file.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
106
passport-cli/src/bin/test_universe_binding.rs
Normal file
106
passport-cli/src/bin/test_universe_binding.rs
Normal 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");
|
||||||
|
}
|
||||||
241
passport-cli/src/cli/commands.rs
Normal file
241
passport-cli/src/cli/commands.rs
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
661
passport-cli/src/cli/interface.rs
Normal file
661
passport-cli/src/cli/interface.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
5
passport-cli/src/cli/mod.rs
Normal file
5
passport-cli/src/cli/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod commands;
|
||||||
|
pub mod interface;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests;
|
||||||
3310
passport-cli/src/cli/tests.rs
Normal file
3310
passport-cli/src/cli/tests.rs
Normal file
File diff suppressed because it is too large
Load diff
121
passport-cli/src/main.rs
Normal file
121
passport-cli/src/main.rs
Normal 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
149
passport/ARCHITECTURE.md
Normal 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
960
passport/Cargo.lock
generated
Normal 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.1"
|
||||||
|
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
66
passport/Cargo.toml
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
[package]
|
||||||
|
name = "passport"
|
||||||
|
version = "0.4.1"
|
||||||
|
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
437
passport/LICENSE
Normal 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
180
passport/README.md
Normal 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/
|
||||||
10
passport/src/application/error.rs
Normal file
10
passport/src/application/error.rs
Normal 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),
|
||||||
|
}
|
||||||
5
passport/src/application/mod.rs
Normal file
5
passport/src/application/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod use_cases;
|
||||||
|
pub mod error;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod use_cases_test;
|
||||||
535
passport/src/application/use_cases.rs
Normal file
535
passport/src/application/use_cases.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
533
passport/src/application/use_cases_test.rs
Normal file
533
passport/src/application/use_cases_test.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
343
passport/src/domain/entities.rs
Normal file
343
passport/src/domain/entities.rs
Normal 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>
|
||||||
|
}
|
||||||
253
passport/src/domain/entities_test.rs
Normal file
253
passport/src/domain/entities_test.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
16
passport/src/domain/error.rs
Normal file
16
passport/src/domain/error.rs
Normal 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),
|
||||||
|
}
|
||||||
6
passport/src/domain/mod.rs
Normal file
6
passport/src/domain/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
pub mod entities;
|
||||||
|
pub mod traits;
|
||||||
|
pub mod error;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod entities_test;
|
||||||
45
passport/src/domain/traits.rs
Normal file
45
passport/src/domain/traits.rs
Normal 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>;
|
||||||
|
}
|
||||||
32
passport/src/infrastructure/crypto/mod.rs
Normal file
32
passport/src/infrastructure/crypto/mod.rs
Normal 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::*;
|
||||||
242
passport/src/infrastructure/crypto/native.rs
Normal file
242
passport/src/infrastructure/crypto/native.rs
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
//! 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)))?;
|
||||||
|
// Ensure we have data to encrypt (even for None values)
|
||||||
|
let date_of_birth_payload = if date_of_birth_bytes.is_empty() {
|
||||||
|
// For None values, use a minimal placeholder to ensure encryption works
|
||||||
|
vec![0u8]
|
||||||
|
} else {
|
||||||
|
date_of_birth_bytes
|
||||||
|
};
|
||||||
|
let enc_date_of_birth = cipher
|
||||||
|
.encrypt(&nonce, &*date_of_birth_payload)
|
||||||
|
.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)))?;
|
||||||
|
// Ensure we have data to encrypt (even for None values)
|
||||||
|
let default_user_profile_id_payload = if default_user_profile_id_bytes.is_empty() {
|
||||||
|
// For None values, use a minimal placeholder to ensure encryption works
|
||||||
|
vec![0u8]
|
||||||
|
} else {
|
||||||
|
default_user_profile_id_bytes
|
||||||
|
};
|
||||||
|
let enc_default_user_profile_id = cipher
|
||||||
|
.encrypt(&nonce, &*default_user_profile_id_payload)
|
||||||
|
.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)))?;
|
||||||
|
// Handle the case where we used a placeholder for None values
|
||||||
|
let date_of_birth: Option<DateOfBirth> = if date_of_birth_bytes == vec![0u8] {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
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)))?;
|
||||||
|
// Handle the case where we used a placeholder for None values
|
||||||
|
let default_user_profile_id: Option<String> = if default_user_profile_id_bytes == vec![0u8] {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
83
passport/src/infrastructure/crypto/native_test.rs
Normal file
83
passport/src/infrastructure/crypto/native_test.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
35
passport/src/infrastructure/crypto/shared.rs
Normal file
35
passport/src/infrastructure/crypto/shared.rs
Normal 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(())
|
||||||
|
}
|
||||||
241
passport/src/infrastructure/crypto/wasm.rs
Normal file
241
passport/src/infrastructure/crypto/wasm.rs
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
//! 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)))?;
|
||||||
|
// Ensure we have data to encrypt (even for None values)
|
||||||
|
let date_of_birth_payload = if date_of_birth_bytes.is_empty() {
|
||||||
|
// For None values, use a minimal placeholder to ensure encryption works
|
||||||
|
vec![0u8]
|
||||||
|
} else {
|
||||||
|
date_of_birth_bytes
|
||||||
|
};
|
||||||
|
let enc_date_of_birth = cipher
|
||||||
|
.encrypt(&nonce, &*date_of_birth_payload)
|
||||||
|
.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)))?;
|
||||||
|
// Ensure we have data to encrypt (even for None values)
|
||||||
|
let default_user_profile_id_payload = if default_user_profile_id_bytes.is_empty() {
|
||||||
|
// For None values, use a minimal placeholder to ensure encryption works
|
||||||
|
vec![0u8]
|
||||||
|
} else {
|
||||||
|
default_user_profile_id_bytes
|
||||||
|
};
|
||||||
|
let enc_default_user_profile_id = cipher
|
||||||
|
.encrypt(&nonce, &*default_user_profile_id_payload)
|
||||||
|
.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)))?;
|
||||||
|
// Handle the case where we used a placeholder for None values
|
||||||
|
let date_of_birth: Option<DateOfBirth> = if date_of_birth_bytes == vec![0u8] {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
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)))?;
|
||||||
|
// Handle the case where we used a placeholder for None values
|
||||||
|
let default_user_profile_id: Option<String> = if default_user_profile_id_bytes == vec![0u8] {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
83
passport/src/infrastructure/crypto/wasm_test.rs
Normal file
83
passport/src/infrastructure/crypto/wasm_test.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
14
passport/src/infrastructure/mod.rs
Normal file
14
passport/src/infrastructure/mod.rs
Normal 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::*;
|
||||||
|
|
||||||
49
passport/src/infrastructure/rng.rs
Normal file
49
passport/src/infrastructure/rng.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
22
passport/src/infrastructure/storage/mod.rs
Normal file
22
passport/src/infrastructure/storage/mod.rs
Normal 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;
|
||||||
58
passport/src/infrastructure/storage/native.rs
Normal file
58
passport/src/infrastructure/storage/native.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
60
passport/src/infrastructure/storage/native_test.rs
Normal file
60
passport/src/infrastructure/storage/native_test.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
110
passport/src/infrastructure/storage/wasm.rs
Normal file
110
passport/src/infrastructure/storage/wasm.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
passport/src/infrastructure/storage/wasm_test.rs
Normal file
59
passport/src/infrastructure/storage/wasm_test.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
52
passport/src/infrastructure/time.rs
Normal file
52
passport/src/infrastructure/time.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
65
passport/src/infrastructure/traits.rs
Normal file
65
passport/src/infrastructure/traits.rs
Normal 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
55
passport/src/lib.rs
Normal 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,
|
||||||
|
};
|
||||||
232
passport/src/universe_binding_test.rs
Normal file
232
passport/src/universe_binding_test.rs
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
use crate::application::use_cases::*;
|
||||||
|
use crate::domain::entities::*;
|
||||||
|
use crate::infrastructure::crypto::*;
|
||||||
|
use crate::infrastructure::storage::*;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod universe_binding_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_passport_creation_with_different_universes() {
|
||||||
|
let mnemonic_generator = Bip39MnemonicGenerator;
|
||||||
|
let key_deriver = Ed25519KeyDeriver;
|
||||||
|
let file_encryptor = XChaCha20FileEncryptor;
|
||||||
|
let file_storage = FileSystemStorage;
|
||||||
|
|
||||||
|
let create_use_case = CreatePassportUseCase::new(
|
||||||
|
mnemonic_generator.clone(),
|
||||||
|
key_deriver.clone(),
|
||||||
|
file_encryptor.clone(),
|
||||||
|
file_storage.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create passports for different universes with the same mnemonic
|
||||||
|
let univ1 = "univ:test:alpha";
|
||||||
|
let univ2 = "univ:test:beta";
|
||||||
|
let password = "test_password";
|
||||||
|
|
||||||
|
// Create first passport
|
||||||
|
let (passport1, recovery_phrase) = create_use_case
|
||||||
|
.execute(univ1, password, "/tmp/test_passport1.spf")
|
||||||
|
.expect("Failed to create passport 1");
|
||||||
|
|
||||||
|
// Create second passport with same mnemonic but different universe
|
||||||
|
let import_use_case = ImportFromRecoveryUseCase::new(
|
||||||
|
mnemonic_generator,
|
||||||
|
key_deriver,
|
||||||
|
file_encryptor,
|
||||||
|
file_storage,
|
||||||
|
);
|
||||||
|
|
||||||
|
let passport2 = import_use_case
|
||||||
|
.execute(
|
||||||
|
univ2,
|
||||||
|
&recovery_phrase.words(),
|
||||||
|
password,
|
||||||
|
"/tmp/test_passport2.spf",
|
||||||
|
)
|
||||||
|
.expect("Failed to create passport 2");
|
||||||
|
|
||||||
|
// Verify universe binding
|
||||||
|
assert_eq!(passport1.univ_id(), univ1);
|
||||||
|
assert_eq!(passport2.univ_id(), univ2);
|
||||||
|
|
||||||
|
// Verify DIDs are universe-bound
|
||||||
|
assert!(passport1.did().as_str().contains(univ1));
|
||||||
|
assert!(passport2.did().as_str().contains(univ2));
|
||||||
|
assert_ne!(passport1.did().as_str(), passport2.did().as_str());
|
||||||
|
|
||||||
|
// Verify public keys are different (due to universe binding)
|
||||||
|
assert_ne!(
|
||||||
|
hex::encode(&passport1.public_key().0),
|
||||||
|
hex::encode(&passport2.public_key().0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
let _ = fs::remove_file("/tmp/test_passport1.spf");
|
||||||
|
let _ = fs::remove_file("/tmp/test_passport2.spf");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_universe_bound_card_signing() {
|
||||||
|
let mnemonic_generator = Bip39MnemonicGenerator;
|
||||||
|
let key_deriver = Ed25519KeyDeriver;
|
||||||
|
let file_encryptor = XChaCha20FileEncryptor;
|
||||||
|
let file_storage = FileSystemStorage;
|
||||||
|
|
||||||
|
let create_use_case = CreatePassportUseCase::new(
|
||||||
|
mnemonic_generator,
|
||||||
|
key_deriver,
|
||||||
|
file_encryptor,
|
||||||
|
file_storage,
|
||||||
|
);
|
||||||
|
|
||||||
|
let univ_id = "univ:test:signing";
|
||||||
|
let password = "test_password";
|
||||||
|
|
||||||
|
let (passport, _) = create_use_case
|
||||||
|
.execute(univ_id, password, "/tmp/test_signing.spf")
|
||||||
|
.expect("Failed to create passport");
|
||||||
|
|
||||||
|
let sign_use_case = SignCardUseCase::new();
|
||||||
|
let message = "Hello, universe!";
|
||||||
|
let signature = sign_use_case
|
||||||
|
.execute(&passport, message)
|
||||||
|
.expect("Failed to sign message");
|
||||||
|
|
||||||
|
// Verify signature is universe-bound
|
||||||
|
let signing_key = ed25519_dalek::SigningKey::from_bytes(
|
||||||
|
&passport.private_key.0[..32].try_into().unwrap()
|
||||||
|
);
|
||||||
|
let verifying_key = signing_key.verifying_key();
|
||||||
|
|
||||||
|
// Correct universe-bound message should verify
|
||||||
|
let correct_message = format!("univ:{}:{}", univ_id, message);
|
||||||
|
assert!(verifying_key
|
||||||
|
.verify_strict(correct_message.as_bytes(), &ed25519_dalek::Signature::from_bytes(&signature).unwrap())
|
||||||
|
.is_ok());
|
||||||
|
|
||||||
|
// Wrong universe message should NOT verify
|
||||||
|
let wrong_message = format!("univ:{}:{}", "univ:wrong:universe", message);
|
||||||
|
assert!(verifying_key
|
||||||
|
.verify_strict(wrong_message.as_bytes(), &ed25519_dalek::Signature::from_bytes(&signature).unwrap())
|
||||||
|
.is_err());
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
let _ = fs::remove_file("/tmp/test_signing.spf");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_passport_file_stores_univ_id() {
|
||||||
|
let mnemonic_generator = Bip39MnemonicGenerator;
|
||||||
|
let key_deriver = Ed25519KeyDeriver;
|
||||||
|
let file_encryptor = XChaCha20FileEncryptor;
|
||||||
|
let file_storage = FileSystemStorage;
|
||||||
|
|
||||||
|
let create_use_case = CreatePassportUseCase::new(
|
||||||
|
mnemonic_generator,
|
||||||
|
key_deriver,
|
||||||
|
file_encryptor.clone(),
|
||||||
|
file_storage.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let univ_id = "univ:test:storage";
|
||||||
|
let password = "test_password";
|
||||||
|
|
||||||
|
let (passport, _) = create_use_case
|
||||||
|
.execute(univ_id, password, "/tmp/test_storage.spf")
|
||||||
|
.expect("Failed to create passport");
|
||||||
|
|
||||||
|
// Load the file and verify univ_id is stored
|
||||||
|
let loaded_file = file_storage
|
||||||
|
.load("/tmp/test_storage.spf")
|
||||||
|
.expect("Failed to load passport file");
|
||||||
|
|
||||||
|
assert_eq!(loaded_file.univ_id, univ_id);
|
||||||
|
|
||||||
|
// Import from file and verify univ_id is preserved
|
||||||
|
let import_use_case = ImportFromFileUseCase::new(file_encryptor, file_storage);
|
||||||
|
let imported_passport = import_use_case
|
||||||
|
.execute("/tmp/test_storage.spf", password, None)
|
||||||
|
.expect("Failed to import passport");
|
||||||
|
|
||||||
|
assert_eq!(imported_passport.univ_id(), univ_id);
|
||||||
|
assert_eq!(imported_passport.did().as_str(), passport.did().as_str());
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
let _ = fs::remove_file("/tmp/test_storage.spf");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cross_universe_prevention() {
|
||||||
|
let mnemonic_generator = Bip39MnemonicGenerator;
|
||||||
|
let key_deriver = Ed25519KeyDeriver;
|
||||||
|
let file_encryptor = XChaCha20FileEncryptor;
|
||||||
|
let file_storage = FileSystemStorage;
|
||||||
|
|
||||||
|
let create_use_case = CreatePassportUseCase::new(
|
||||||
|
mnemonic_generator.clone(),
|
||||||
|
key_deriver.clone(),
|
||||||
|
file_encryptor.clone(),
|
||||||
|
file_storage.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let univ1 = "univ:test:security1";
|
||||||
|
let univ2 = "univ:test:security2";
|
||||||
|
let password = "test_password";
|
||||||
|
|
||||||
|
// Create passport for universe 1
|
||||||
|
let (passport1, recovery_phrase) = create_use_case
|
||||||
|
.execute(univ1, password, "/tmp/test_security1.spf")
|
||||||
|
.expect("Failed to create passport 1");
|
||||||
|
|
||||||
|
// Try to import same mnemonic into universe 2
|
||||||
|
let import_use_case = ImportFromRecoveryUseCase::new(
|
||||||
|
mnemonic_generator,
|
||||||
|
key_deriver,
|
||||||
|
file_encryptor,
|
||||||
|
file_storage,
|
||||||
|
);
|
||||||
|
|
||||||
|
let passport2 = import_use_case
|
||||||
|
.execute(
|
||||||
|
univ2,
|
||||||
|
&recovery_phrase.words(),
|
||||||
|
password,
|
||||||
|
"/tmp/test_security2.spf",
|
||||||
|
)
|
||||||
|
.expect("Failed to create passport 2");
|
||||||
|
|
||||||
|
// Verify they are completely different identities
|
||||||
|
assert_ne!(passport1.univ_id(), passport2.univ_id());
|
||||||
|
assert_ne!(passport1.did().as_str(), passport2.did().as_str());
|
||||||
|
assert_ne!(
|
||||||
|
hex::encode(&passport1.public_key().0),
|
||||||
|
hex::encode(&passport2.public_key().0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cards signed by passport1 should not be verifiable by passport2 and vice versa
|
||||||
|
let sign_use_case = SignCardUseCase::new();
|
||||||
|
let message = "Cross-universe test";
|
||||||
|
let signature1 = sign_use_case
|
||||||
|
.execute(&passport1, message)
|
||||||
|
.expect("Failed to sign with passport1");
|
||||||
|
|
||||||
|
// Verify signature1 cannot be verified with passport2's public key
|
||||||
|
let signing_key2 = ed25519_dalek::SigningKey::from_bytes(
|
||||||
|
&passport2.private_key.0[..32].try_into().unwrap()
|
||||||
|
);
|
||||||
|
let verifying_key2 = signing_key2.verifying_key();
|
||||||
|
|
||||||
|
let message_for_univ1 = format!("univ:{}:{}", univ1, message);
|
||||||
|
assert!(verifying_key2
|
||||||
|
.verify_strict(message_for_univ1.as_bytes(), &ed25519_dalek::Signature::from_bytes(&signature1).unwrap())
|
||||||
|
.is_err());
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
let _ = fs::remove_file("/tmp/test_security1.spf");
|
||||||
|
let _ = fs::remove_file("/tmp/test_security2.spf");
|
||||||
|
}
|
||||||
|
}
|
||||||
346
passport/src/wasm.rs
Normal file
346
passport/src/wasm.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Reference in a new issue