#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use alloc::vec;
use alloc::vec::Vec;
#[cfg(feature = "std")]
use std::{
fs::File,
io::{Read, Write},
};
use aes_gcm::{
Aes256Gcm, Nonce,
aead::{Aead, KeyInit},
};
use argon2::Argon2;
use thiserror::Error;
use rand::{CryptoRng, Rng};
#[cfg(feature = "zeroize")]
use zeroize::Zeroizing;
#[derive(Error, Debug)]
pub enum CryptError {
#[error("failed to hash password")]
HashingError,
#[error("encryption error: {0}")]
EncryptionError(#[from] aes_gcm::Error),
#[error("invalid key or nonce length")]
InvalidLength,
#[cfg(feature = "std")]
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("invalid data format: {0}")]
InvalidData(&'static str),
}
#[derive(Debug)]
pub struct CryptData {
pub ciphertext: Vec<u8>,
pub salt: Vec<u8>,
pub nonce_bytes: Vec<u8>,
}
pub fn encrypt_data(
password: &str,
plaintext: &[u8],
rng: &mut (impl Rng + CryptoRng),
) -> Result<CryptData, CryptError> {
let mut salt = vec![0u8; 16];
rng.fill_bytes(&mut salt);
#[cfg(feature = "zeroize")]
let mut derived_key = Zeroizing::new(vec![0u8; 32]);
#[cfg(not(feature = "zeroize"))]
let mut derived_key = vec![0u8; 32];
Argon2::default()
.hash_password_into(password.as_bytes(), &salt, derived_key.as_mut_slice())
.map_err(|_| CryptError::HashingError)?;
let cipher = Aes256Gcm::new_from_slice(&derived_key).map_err(|_| CryptError::InvalidLength)?;
let mut nonce_bytes = vec![0u8; 12];
rng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::try_from(nonce_bytes.as_slice()).map_err(|_| CryptError::InvalidLength)?;
let ciphertext = cipher.encrypt(&nonce, plaintext)?;
Ok(CryptData {
ciphertext,
salt,
nonce_bytes,
})
}
pub fn decrypt_data(data: &CryptData, password: &str) -> Result<Vec<u8>, CryptError> {
decrypt_parts(
&data.salt,
&data.nonce_bytes,
&data.ciphertext,
password,
)
}
pub fn decrypt_bytes(data: &[u8], password: &str) -> Result<Vec<u8>, CryptError> {
if data.len() < 44 {
return Err(CryptError::InvalidData(
"File is too short to contain valid encrypted data",
));
}
let salt = &data[0..16];
let nonce_bytes = &data[16..28];
let ciphertext = &data[28..];
decrypt_parts(salt, nonce_bytes, ciphertext, password)
}
fn decrypt_parts(
salt: &[u8],
nonce_bytes: &[u8],
ciphertext: &[u8],
password: &str,
) -> Result<Vec<u8>, CryptError> {
if salt.len() != 16 || nonce_bytes.len() != 12 || ciphertext.len() < 16 {
return Err(CryptError::InvalidData("Invalid encrypted data"));
}
#[cfg(feature = "zeroize")]
let mut derived_key = Zeroizing::new(vec![0u8; 32]);
#[cfg(not(feature = "zeroize"))]
let mut derived_key = vec![0u8; 32];
Argon2::default()
.hash_password_into(password.as_bytes(), salt, derived_key.as_mut_slice())
.map_err(|_| CryptError::HashingError)?;
let cipher = Aes256Gcm::new_from_slice(&derived_key).map_err(|_| CryptError::InvalidLength)?;
let nonce = Nonce::try_from(nonce_bytes).map_err(|_| CryptError::InvalidLength)?;
let plaintext = cipher.decrypt(&nonce, ciphertext)?;
Ok(plaintext)
}
#[cfg(feature = "std")]
pub fn save_file(file_path: &str, data: &CryptData) -> Result<(), CryptError> {
let mut file = File::create(file_path)?;
file.write_all(&data.salt)?;
file.write_all(&data.nonce_bytes)?;
file.write_all(&data.ciphertext)?;
Ok(())
}
#[cfg(feature = "std")]
pub fn decrypt_file(file_path: &str, password: &str) -> Result<Vec<u8>, CryptError> {
let mut file = File::open(file_path)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
decrypt_bytes(&buffer, password)
}
#[cfg(test)]
mod tests {
use super::*;
fn t_encrypt_file() -> Result<(), CryptError> {
let mut rng = rand::rng();
let data = encrypt_data("password", "super secret stuff".as_bytes(), &mut rng)?;
let _ = save_file("secret.stuff", &data)?;
Ok(())
}
fn t_decrypt_file() -> Result<(), CryptError> {
let data =
alloc::string::String::from_utf8(decrypt_file("secret.stuff", "password")?).unwrap();
assert_eq!(data, "super secret stuff");
Ok(())
}
#[test]
fn test_encrypt_decrypt() -> Result<(), CryptError> {
t_encrypt_file()?;
t_decrypt_file()?;
let _ = std::fs::remove_file("secret.stuff");
Ok(())
}
}