use crate::crypto::CryptoSecret;
use crate::error::{Error, Result};
use aes_gcm::aead::{generic_array::GenericArray, Aead};
use hkdf::Hkdf;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use zeroize::Zeroize;
#[derive(Debug, Clone)]
pub struct EncryptionConfig {
pub app_identifier: String,
pub service_name: String,
pub require_auth_every_use: bool,
pub auth_timeout_seconds: u32,
pub allow_device_passcode_fallback: bool,
}
impl Default for EncryptionConfig {
fn default() -> Self {
Self {
app_identifier: "com.webycash.webylib".to_string(),
service_name: "WalletEncryption".to_string(),
require_auth_every_use: true,
auth_timeout_seconds: 0,
allow_device_passcode_fallback: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptedData {
pub ciphertext: Vec<u8>,
pub nonce: [u8; 12],
pub salt: [u8; 32],
pub algorithm: String,
pub kdf_params: KdfParams,
pub metadata: EncryptionMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KdfParams {
pub info: String,
pub iterations: u32,
pub memory_cost: u32,
pub parallelism: u32,
}
impl Default for KdfParams {
fn default() -> Self {
Self {
info: "webycash-biometric-v1".to_string(),
iterations: 100_000,
memory_cost: 65536, parallelism: 4,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptionMetadata {
pub encrypted_at: String,
pub platform: String,
pub version: String,
pub biometric_type: Option<String>,
}
pub struct BiometricEncryption {
#[allow(dead_code)] config: EncryptionConfig,
cached_key: Option<CryptoSecret>,
}
impl BiometricEncryption {
pub fn new(config: EncryptionConfig) -> Result<Self> {
Ok(Self {
config,
cached_key: None,
})
}
pub async fn encrypt_with_biometrics(&mut self, plaintext: &[u8]) -> Result<EncryptedData> {
let mut salt = [0u8; 32];
getrandom::getrandom(&mut salt)
.map_err(|e| Error::crypto(format!("Failed to generate salt: {}", e)))?;
let master_key = self.get_or_create_biometric_key().await?;
let encryption_key = self.derive_encryption_key(&master_key, &salt)?;
let cipher = encryption_key.create_cipher();
let mut nonce_bytes = [0u8; 12];
getrandom::getrandom(&mut nonce_bytes)
.map_err(|e| Error::crypto(format!("Failed to generate nonce: {}", e)))?;
let nonce = GenericArray::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, plaintext)
.map_err(|e| Error::crypto(format!("Encryption failed: {}", e)))?;
let metadata = EncryptionMetadata {
encrypted_at: format!(
"{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
),
platform: self.get_platform_name(),
version: "1.0".to_string(),
biometric_type: self.get_available_biometric_type().await,
};
Ok(EncryptedData {
ciphertext,
nonce: nonce_bytes,
salt,
algorithm: "AES-256-GCM".to_string(),
kdf_params: KdfParams::default(),
metadata,
})
}
pub async fn decrypt_with_biometrics(
&mut self,
encrypted_data: &EncryptedData,
) -> Result<Vec<u8>> {
if encrypted_data.algorithm != "AES-256-GCM" {
return Err(Error::crypto("Unsupported encryption algorithm"));
}
let master_key = self.authenticate_and_get_key().await?;
let decryption_key = self.derive_encryption_key(&master_key, &encrypted_data.salt)?;
let cipher = decryption_key.create_cipher();
let nonce = GenericArray::from_slice(&encrypted_data.nonce);
let plaintext = cipher
.decrypt(nonce, encrypted_data.ciphertext.as_slice())
.map_err(|e| Error::crypto(format!("Decryption failed: {}", e)))?;
Ok(plaintext)
}
pub fn clear_cached_keys(&mut self) {
if let Some(mut key) = self.cached_key.take() {
key.zeroize();
}
}
pub async fn is_biometric_available(&self) -> Result<bool> {
#[cfg(target_os = "ios")]
{
self.is_biometric_available_ios().await
}
#[cfg(target_os = "android")]
{
self.is_biometric_available_android().await
}
#[cfg(not(any(target_os = "ios", target_os = "android")))]
{
Ok(false)
}
}
pub async fn get_available_biometric_type(&self) -> Option<String> {
#[cfg(target_os = "ios")]
{
self.get_ios_biometric_type().await
}
#[cfg(target_os = "android")]
{
self.get_android_biometric_type().await
}
#[cfg(not(any(target_os = "ios", target_os = "android")))]
{
None
}
}
async fn get_or_create_biometric_key(&mut self) -> Result<CryptoSecret> {
if let Some(ref key) = self.cached_key {
return Ok(key.clone());
}
match self.retrieve_biometric_key().await {
Ok(key) => {
self.cached_key = Some(key.clone());
Ok(key)
}
Err(_) => {
let key = CryptoSecret::generate()
.map_err(|e| Error::crypto(format!("Failed to generate master key: {}", e)))?;
self.store_biometric_key(&key).await?;
self.cached_key = Some(key.clone());
Ok(key)
}
}
}
async fn authenticate_and_get_key(&mut self) -> Result<CryptoSecret> {
if let Some(ref key) = self.cached_key {
if self.verify_biometric_access().await? {
return Ok(key.clone());
} else {
self.clear_cached_keys();
}
}
let key = self.retrieve_biometric_key().await?;
self.cached_key = Some(key.clone());
Ok(key)
}
fn derive_encryption_key(
&self,
master_key: &CryptoSecret,
salt: &[u8; 32],
) -> Result<CryptoSecret> {
let hk = Hkdf::<Sha256>::new(Some(salt), master_key.as_bytes());
let mut okm = [0u8; 32];
hk.expand(b"webycash-biometric-v1", &mut okm)
.map_err(|e| Error::crypto(format!("Key derivation failed: {}", e)))?;
Ok(CryptoSecret::from_bytes(okm))
}
fn get_platform_name(&self) -> String {
#[cfg(target_os = "ios")]
return "ios".to_string();
#[cfg(target_os = "android")]
return "android".to_string();
#[cfg(not(any(target_os = "ios", target_os = "android")))]
return "other".to_string();
}
#[cfg(target_os = "ios")]
async fn is_biometric_available_ios(&self) -> Result<bool> {
Ok(false)
}
#[cfg(target_os = "ios")]
async fn get_ios_biometric_type(&self) -> Option<String> {
None
}
#[cfg(target_os = "ios")]
async fn store_biometric_key(&self, _key: &CryptoSecret) -> Result<()> {
Err(Error::crypto("iOS biometric storage not yet implemented"))
}
#[cfg(target_os = "ios")]
async fn retrieve_biometric_key(&self) -> Result<CryptoSecret> {
Err(Error::crypto("iOS biometric retrieval not yet implemented"))
}
#[cfg(target_os = "ios")]
async fn verify_biometric_access(&self) -> Result<bool> {
Ok(false)
}
#[cfg(target_os = "android")]
async fn is_biometric_available_android(&self) -> Result<bool> {
Ok(false)
}
#[cfg(target_os = "android")]
async fn get_android_biometric_type(&self) -> Option<String> {
None
}
#[cfg(target_os = "android")]
async fn store_biometric_key(&self, _key: &CryptoSecret) -> Result<()> {
Err(Error::crypto(
"Android biometric storage not yet implemented",
))
}
#[cfg(target_os = "android")]
async fn retrieve_biometric_key(&self) -> Result<CryptoSecret> {
Err(Error::crypto(
"Android biometric retrieval not yet implemented",
))
}
#[cfg(target_os = "android")]
async fn verify_biometric_access(&self) -> Result<bool> {
Ok(false)
}
#[cfg(not(any(target_os = "ios", target_os = "android")))]
async fn store_biometric_key(&self, _key: &CryptoSecret) -> Result<()> {
Err(Error::crypto(
"Biometric storage not supported on this platform",
))
}
#[cfg(not(any(target_os = "ios", target_os = "android")))]
async fn retrieve_biometric_key(&self) -> Result<CryptoSecret> {
Err(Error::crypto(
"Biometric storage not supported on this platform",
))
}
#[cfg(not(any(target_os = "ios", target_os = "android")))]
async fn verify_biometric_access(&self) -> Result<bool> {
Ok(false)
}
}
impl Drop for BiometricEncryption {
fn drop(&mut self) {
self.clear_cached_keys();
}
}
pub fn encrypt_with_password(plaintext: &[u8], password: &str) -> Result<EncryptedData> {
let mut salt = [0u8; 32];
getrandom::getrandom(&mut salt)
.map_err(|e| Error::crypto(format!("Failed to generate salt: {}", e)))?;
let mut key_bytes = [0u8; 32];
argon2::Argon2::default()
.hash_password_into(password.as_bytes(), &salt, &mut key_bytes)
.map_err(|e| Error::crypto(format!("Password key derivation failed: {}", e)))?;
let encryption_key = CryptoSecret::from_bytes(key_bytes);
let cipher = encryption_key.create_cipher();
let mut nonce_bytes = [0u8; 12];
getrandom::getrandom(&mut nonce_bytes)
.map_err(|e| Error::crypto(format!("Failed to generate nonce: {}", e)))?;
let nonce = GenericArray::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, plaintext)
.map_err(|e| Error::crypto(format!("Password encryption failed: {}", e)))?;
let metadata = EncryptionMetadata {
encrypted_at: format!(
"{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
),
platform: "password".to_string(),
version: "1.0".to_string(),
biometric_type: None,
};
Ok(EncryptedData {
ciphertext,
nonce: nonce_bytes,
salt,
algorithm: "AES-256-GCM-PASSWORD".to_string(),
kdf_params: KdfParams {
info: "webycash-password-v1".to_string(),
iterations: 0, memory_cost: 65536,
parallelism: 4,
},
metadata,
})
}
pub fn decrypt_with_password(encrypted_data: &EncryptedData, password: &str) -> Result<Vec<u8>> {
if encrypted_data.algorithm != "AES-256-GCM-PASSWORD" {
return Err(Error::crypto("Wrong decryption method for this data"));
}
let mut key_bytes = [0u8; 32];
argon2::Argon2::default()
.hash_password_into(password.as_bytes(), &encrypted_data.salt, &mut key_bytes)
.map_err(|e| Error::crypto(format!("Password key derivation failed: {}", e)))?;
let decryption_key = CryptoSecret::from_bytes(key_bytes);
let cipher = decryption_key.create_cipher();
let nonce = GenericArray::from_slice(&encrypted_data.nonce);
let plaintext = cipher
.decrypt(nonce, encrypted_data.ciphertext.as_slice())
.map_err(|e| Error::crypto(format!("Password decryption failed: {}", e)))?;
Ok(plaintext)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_password_encryption_roundtrip() {
let plaintext = b"Hello, secure world!";
let password = "test_password_123";
let encrypted = encrypt_with_password(plaintext, password).unwrap();
assert_eq!(encrypted.algorithm, "AES-256-GCM-PASSWORD");
assert_eq!(encrypted.nonce.len(), 12);
assert_eq!(encrypted.salt.len(), 32);
let decrypted = decrypt_with_password(&encrypted, password).unwrap();
assert_eq!(decrypted, plaintext);
let wrong_result = decrypt_with_password(&encrypted, "wrong_password");
assert!(wrong_result.is_err());
}
#[tokio::test]
async fn test_biometric_encryption_config() {
let config = EncryptionConfig::default();
let biometric = BiometricEncryption::new(config);
assert!(biometric.is_ok());
}
#[test]
fn test_crypto_secret_security() {
let secret = CryptoSecret::generate().unwrap();
let debug_str = format!("{:?}", secret);
assert_eq!(debug_str, "CryptoSecret([REDACTED])");
let display_str = format!("{}", secret);
assert_eq!(display_str, "[REDACTED 32-byte secret]");
}
}