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
217 lines
No EOL
8.9 KiB
Rust
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))
|
|
}
|
|
} |