sharenet/passport/src/infrastructure/crypto/wasm.rs
continuist 05674b4caa
Some checks failed
Podman Rootless Demo / test-backend (push) Has been skipped
Podman Rootless Demo / test-frontend (push) Has been skipped
Podman Rootless Demo / build-backend (push) Has been skipped
Podman Rootless Demo / build-frontend (push) Failing after 5m32s
Podman Rootless Demo / deploy-prod (push) Has been skipped
Add to project
2025-11-01 11:53:11 -04:00

217 lines
No EOL
8.9 KiB
Rust

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