Implement restructuring

This commit is contained in:
Continuist 2025-10-04 14:46:33 -04:00
parent dc3609bd75
commit 1a0f454c71
27 changed files with 1555 additions and 262 deletions

15
Cargo.lock generated
View file

@ -632,16 +632,13 @@ dependencies = [
name = "sharenet-passport"
version = "0.1.0"
dependencies = [
"assert_matches",
"bip39",
"chacha20poly1305",
"clap",
"ed25519-dalek",
"hex",
"hkdf",
"rand",
"rand_core",
"rpassword",
"serde",
"serde_cbor",
"sha2",
@ -650,6 +647,18 @@ dependencies = [
"zeroize",
]
[[package]]
name = "sharenet-passport-cli"
version = "0.1.0"
dependencies = [
"assert_matches",
"clap",
"hex",
"rpassword",
"sharenet-passport",
"tempfile",
]
[[package]]
name = "signature"
version = "2.2.0"

View file

@ -1,9 +1,12 @@
[package]
name = "sharenet-passport"
version = "0.1.0"
edition = "2021"
[workspace]
members = [
"libs/sharenet-passport",
"sharenet-passport-cli"
]
resolver = "2"
[dependencies]
[workspace.dependencies]
# Common dependencies shared across workspace members
bip39 = "2.1"
ed25519-dalek = { version = "2.1", features = ["serde"] }
chacha20poly1305 = "0.10"
@ -13,13 +16,8 @@ rand = "0.8"
rand_core = "0.6"
serde = { version = "1.0", features = ["derive"] }
serde_cbor = "0.11"
clap = { version = "4.4", features = ["derive"] }
thiserror = "1.0"
zeroize = { version = "1.7", features = ["zeroize_derive"] }
rpassword = "7.2"
hex = "0.4"
[dev-dependencies]
assert_matches = "1.5"
hex = "0.4"
tempfile = "3.8"
clap = { version = "4.4", features = ["derive"] }
rpassword = "7.2"

View file

@ -24,7 +24,7 @@ cd sharenet_passport_creator
cargo build --release
# Run the CLI
./target/release/sharenet-passport --help
./target/release/sharenet-passport-cli-cli --help
```
## Usage Guide
@ -34,7 +34,7 @@ cargo build --release
Create a new Sharenet Passport with a secure recovery phrase:
```bash
./target/release/sharenet-passport create my-passport.spf
./target/release/sharenet-passport-cli create --output my-passport.spf
```
You'll be prompted to:
@ -54,7 +54,7 @@ The tool will then:
If you have a 24-word recovery phrase, you can import it to create a new .spf file:
```bash
./target/release/sharenet-passport import-recovery recovered-passport.spf
./target/release/sharenet-passport-cli import-recovery --output recovered-passport.spf
```
You'll be prompted to:
@ -67,10 +67,10 @@ Import an existing .spf file (useful for re-encryption or verification):
```bash
# Import without re-encryption
./target/release/sharenet-passport import-file existing.spf
./target/release/sharenet-passport-cli import-file existing.spf
# Import and re-encrypt to new file
./target/release/sharenet-passport import-file existing.spf new-passport.spf
./target/release/sharenet-passport-cli import-file existing.spf --output new-passport.spf
```
You'll be prompted for the access password of the existing file.
@ -80,7 +80,7 @@ You'll be prompted for the access password of the existing file.
Export a passport with a new access password:
```bash
./target/release/sharenet-passport export old-passport.spf new-passport.spf
./target/release/sharenet-passport-cli export old-passport.spf --output new-passport.spf
```
You'll be prompted to:
@ -92,7 +92,7 @@ You'll be prompted to:
Display information about a .spf file:
```bash
./target/release/sharenet-passport info my-passport.spf
./target/release/sharenet-passport-cli info my-passport.spf
```
Shows:
@ -105,7 +105,7 @@ Shows:
Sign a message using your passport's private key:
```bash
./target/release/sharenet-passport sign my-passport.spf "Hello, Sharenet!"
./target/release/sharenet-passport-cli sign my-passport.spf "Hello, Sharenet!"
```
Output includes:
@ -144,17 +144,30 @@ Your 24-word recovery phrase is the master key to your identity:
### Running Tests
```bash
cargo test
# Run tests for all workspace crates
cargo test --workspace
# Run tests for specific crates
cargo test -p sharenet-passport # Library crate tests
cargo test -p sharenet-passport-cli # CLI crate tests
```
### Project Structure
```
src/
├── domain/ # Core entities and traits
├── application/ # Use cases and business logic
├── infrastructure/ # Crypto and storage implementations
└── cli/ # Command-line interface
sharenet_passport_creator/
├── libs/
│ └── sharenet-passport/ # Core library crate
│ ├── src/
│ │ ├── domain/ # Core entities and traits
│ │ ├── application/ # Use cases and business logic
│ │ └── infrastructure/ # Crypto and storage implementations
│ └── Cargo.toml
├── sharenet-passport-cli/ # CLI crate
│ ├── src/
│ │ └── cli/ # Command-line interface
│ └── Cargo.toml
└── Cargo.toml # Workspace configuration
```
### Architecture

View file

@ -0,0 +1,37 @@
[package]
name = "sharenet-passport"
version = "0.1.0"
edition = "2021"
description = "Core library for Sharenet Passport creation and management"
authors = ["Your Name <your.email@example.com>"]
license = "MIT OR Apache-2.0"
repository = "https://git.sharenet.sh/your-org/sharenet-passport"
[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"
[lib]
crate-type = ["cdylib", "rlib"] # Support both native and WASM
[features]
default = ["std"]
std = [] # Standard library support
alloc = [] # No-std with alloc support
wasm = ["alloc"] # WASM target support
[dev-dependencies]
tempfile = "3.8"
[publish]
registry = "sharenet"

View file

@ -5,10 +5,6 @@ pub enum ApplicationError {
#[error("Use case error: {0}")]
UseCaseError(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Domain error: {0}")]
DomainError(#[from] crate::domain::error::DomainError),
}

View file

@ -2,4 +2,4 @@ pub mod use_cases;
pub mod error;
#[cfg(test)]
pub mod use_cases_test;
mod use_cases_test;

View file

@ -2,12 +2,15 @@ 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("Cryptographic error: {0}")]
CryptographicError(String),
#[error("File operation failed: {0}")]
FileOperationError(String),
}

View file

@ -3,4 +3,4 @@ pub mod traits;
pub mod error;
#[cfg(test)]
pub mod entities_test;
mod entities_test;

View file

@ -2,6 +2,8 @@
mod tests {
use super::*;
use crate::domain::entities::*;
use crate::domain::traits::{MnemonicGenerator, KeyDeriver, FileEncryptor};
use crate::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor};
#[test]
fn test_bip39_generator_creates_valid_mnemonic() {
@ -54,9 +56,10 @@ mod tests {
#[test]
fn test_file_encryptor_round_trip() {
let encryptor = XChaCha20FileEncryptor;
let key_deriver = Ed25519KeyDeriver;
let seed = Seed::new(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
let public_key = PublicKey(vec![1; 32]);
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";

View file

@ -0,0 +1,15 @@
#[cfg(feature = "std")]
pub mod crypto;
#[cfg(feature = "std")]
pub mod storage;
#[cfg(feature = "std")]
pub use crypto::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor};
#[cfg(feature = "std")]
pub use storage::FileSystemStorage;
#[cfg(all(test, feature = "std"))]
mod crypto_test;
#[cfg(all(test, feature = "std"))]
mod storage_test;

View file

@ -2,6 +2,8 @@
mod tests {
use super::*;
use tempfile::NamedTempFile;
use crate::domain::traits::FileStorage;
use crate::{FileSystemStorage, PassportFile};
#[test]
fn test_file_storage_round_trip() {

View file

@ -0,0 +1,29 @@
//! 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;
// 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;
pub use infrastructure::{
Bip39MnemonicGenerator,
Ed25519KeyDeriver,
XChaCha20FileEncryptor,
FileSystemStorage
};

View file

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

View file

@ -0,0 +1,163 @@
use sharenet_passport::{
application::use_cases::*,
infrastructure::*,
ApplicationError,
FileStorage,
};
use rpassword::prompt_password;
use hex;
pub struct CliInterface;
impl CliInterface {
pub fn new() -> Self {
Self
}
pub fn handle_create(&self, output: &str) -> Result<(), ApplicationError> {
let password = prompt_password("Enter password for new passport: ").unwrap();
let confirm_password = prompt_password("Confirm password: ").unwrap();
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(&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, output: &str) -> Result<(), ApplicationError> {
println!("Enter your 24-word recovery phrase:");
let mut recovery_words = Vec::new();
for i in 1..=24 {
let word = prompt_password(&format!("Word {}: ", i)).unwrap();
recovery_words.push(word);
}
let password = prompt_password("Enter new password for passport file: ").unwrap();
let confirm_password = prompt_password("Confirm password: ").unwrap();
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(&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: ").unwrap();
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: ").unwrap();
let new_password = prompt_password("Enter new password for exported file: ").unwrap();
let confirm_password = prompt_password("Confirm new password: ").unwrap();
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!(" 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_sign(&self, file: &str, message: &str) -> Result<(), ApplicationError> {
let password = prompt_password("Enter password for passport file: ").unwrap();
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(())
}
}

View file

@ -1,15 +1,11 @@
mod application;
mod cli;
mod domain;
mod infrastructure;
use clap::Parser;
use crate::cli::commands::{Cli, Commands};
use crate::cli::interface::CliInterface;
use crate::application::error::ApplicationError;
fn main() -> Result<(), ApplicationError> {
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let interface = CliInterface::new();

1231
sharenet_spec.md Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,216 +0,0 @@
use std::io::{self, Write};
use crate::application::use_cases::*;
use crate::application::error::ApplicationError;
use crate::infrastructure::crypto::{Bip39MnemonicGenerator, Ed25519KeyDeriver, XChaCha20FileEncryptor};
use crate::infrastructure::storage::FileSystemStorage;
pub struct CliInterface {
mnemonic_generator: Bip39MnemonicGenerator,
key_deriver: Ed25519KeyDeriver,
file_encryptor: XChaCha20FileEncryptor,
file_storage: FileSystemStorage,
}
impl CliInterface {
pub fn new() -> Self {
Self {
mnemonic_generator: Bip39MnemonicGenerator,
key_deriver: Ed25519KeyDeriver,
file_encryptor: XChaCha20FileEncryptor,
file_storage: FileSystemStorage,
}
}
pub fn prompt_password(&self) -> Result<String, ApplicationError> {
print!("Enter Access Password: ");
io::stdout().flush().map_err(|e| ApplicationError::UseCaseError(format!("Failed to flush stdout: {}", e)))?;
let password = rpassword::read_password()
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
print!("Confirm Access Password: ");
io::stdout().flush().map_err(|e| ApplicationError::UseCaseError(format!("Failed to flush stdout: {}", e)))?;
let password_confirm = rpassword::read_password()
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
if password != password_confirm {
return Err(ApplicationError::InvalidInput("Passwords do not match".to_string()));
}
if password.is_empty() {
return Err(ApplicationError::InvalidInput("Password cannot be empty".to_string()));
}
Ok(password)
}
pub fn prompt_recovery_phrase(&self) -> Result<Vec<String>, ApplicationError> {
println!("Enter your 24-word Recovery Phrase (one word per line):");
let mut words = Vec::new();
for i in 1..=24 {
print!("Word {}: ", i);
io::stdout().flush().map_err(|e| ApplicationError::UseCaseError(format!("Failed to flush stdout: {}", e)))?;
let mut word = String::new();
io::stdin().read_line(&mut word)
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read input: {}", e)))?;
let word = word.trim().to_string();
if word.is_empty() {
return Err(ApplicationError::InvalidInput(format!("Word {} cannot be empty", i)));
}
words.push(word);
}
Ok(words)
}
pub fn handle_create(&self, output_path: &str) -> Result<(), ApplicationError> {
let password = self.prompt_password()?;
let use_case = CreatePassportUseCase::new(
self.mnemonic_generator.clone(),
self.key_deriver.clone(),
self.file_encryptor.clone(),
self.file_storage.clone(),
);
let (passport, recovery_phrase) = use_case.execute(&password, output_path)?;
println!("\n✅ Passport created successfully!");
println!("\n📄 Passport saved to: {}", output_path);
println!("\n🔑 Your DID: {}", passport.did().as_str());
println!("\n📝 Your 24-word Recovery Phrase:");
println!("{}", recovery_phrase.to_string());
println!("\n⚠️ IMPORTANT: Store this Recovery Phrase securely offline!");
println!(" It can regenerate your identity if you lose access.");
Ok(())
}
pub fn handle_import_recovery(&self, output_path: &str) -> Result<(), ApplicationError> {
let recovery_words = self.prompt_recovery_phrase()?;
let password = self.prompt_password()?;
let use_case = ImportFromRecoveryUseCase::new(
self.mnemonic_generator.clone(),
self.key_deriver.clone(),
self.file_encryptor.clone(),
self.file_storage.clone(),
);
let passport = use_case.execute(&recovery_words, &password, output_path)?;
println!("\n✅ Passport imported successfully!");
println!("\n📄 Passport saved to: {}", output_path);
println!("\n🔑 Your DID: {}", passport.did().as_str());
Ok(())
}
pub fn handle_import_file(&self, input_path: &str, output_path: Option<&str>) -> Result<(), ApplicationError> {
print!("Enter Access Password for {}: ", input_path);
io::stdout().flush().map_err(|e| ApplicationError::UseCaseError(format!("Failed to flush stdout: {}", e)))?;
let password = rpassword::read_password()
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
let use_case = ImportFromFileUseCase::new(
self.file_encryptor.clone(),
self.file_storage.clone(),
);
let passport = use_case.execute(input_path, &password, output_path)?;
println!("\n✅ Passport imported successfully!");
println!("\n🔑 Your DID: {}", passport.did().as_str());
if let Some(output_path) = output_path {
println!("\n📄 Re-encrypted passport saved to: {}", output_path);
}
Ok(())
}
pub fn handle_export(&self, input_path: &str, output_path: &str) -> Result<(), ApplicationError> {
print!("Enter Access Password for {}: ", input_path);
io::stdout().flush().map_err(|e| ApplicationError::UseCaseError(format!("Failed to flush stdout: {}", e)))?;
let password = rpassword::read_password()
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
let new_password = self.prompt_password()?;
// Load the passport first
let import_use_case = ImportFromFileUseCase::new(
self.file_encryptor.clone(),
self.file_storage.clone(),
);
let passport = import_use_case.execute(input_path, &password, None)?;
// Export with new password
let export_use_case = ExportPassportUseCase::new(
self.file_encryptor.clone(),
self.file_storage.clone(),
);
export_use_case.execute(&passport, &new_password, output_path)?;
println!("\n✅ Passport exported successfully!");
println!("\n📄 New passport saved to: {}", output_path);
Ok(())
}
pub fn handle_info(&self, file_path: &str) -> Result<(), ApplicationError> {
print!("Enter Access Password for {}: ", file_path);
io::stdout().flush().map_err(|e| ApplicationError::UseCaseError(format!("Failed to flush stdout: {}", e)))?;
let password = rpassword::read_password()
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
let use_case = ImportFromFileUseCase::new(
self.file_encryptor.clone(),
self.file_storage.clone(),
);
let passport = use_case.execute(file_path, &password, None)?;
println!("\n📋 Passport Information:");
println!("🔑 DID: {}", passport.did().as_str());
println!("🔐 Public Key: {}", hex::encode(passport.public_key().0.clone()));
println!("📄 Source: {}", file_path);
Ok(())
}
pub fn handle_sign(&self, file_path: &str, message: &str) -> Result<(), ApplicationError> {
print!("Enter Access Password for {}: ", file_path);
io::stdout().flush().map_err(|e| ApplicationError::UseCaseError(format!("Failed to flush stdout: {}", e)))?;
let password = rpassword::read_password()
.map_err(|e| ApplicationError::UseCaseError(format!("Failed to read password: {}", e)))?;
let use_case = ImportFromFileUseCase::new(
self.file_encryptor.clone(),
self.file_storage.clone(),
);
let passport = use_case.execute(file_path, &password, None)?;
// Sign the message using the SignCardUseCase
let sign_use_case = SignCardUseCase::new();
let signature = sign_use_case.execute(&passport, message)?;
println!("\n✅ Message signed successfully!");
println!("📝 Message: {}", message);
println!("🔐 Signature: {}", hex::encode(&signature));
println!("🔑 Public Key: {}", hex::encode(passport.public_key().0.clone()));
Ok(())
}
}

View file

@ -1,2 +0,0 @@
pub mod crypto;
pub mod storage;