use anyhow::{Context, Result};
use std::io::{Read, Write};
pub fn encrypt(plaintext: &[u8], passphrase: Option<&String>) -> Result<Vec<u8>> {
let passphrase_str = match passphrase {
Some(p) if !p.is_empty() => p.as_str(),
_ => {
return Ok(plaintext.to_vec());
}
};
let encryptor =
age::Encryptor::with_user_passphrase(secrecy::SecretString::from(passphrase_str));
let mut encrypted = vec![];
let mut writer = encryptor
.wrap_output(&mut encrypted)
.context("Failed to create age encryptor")?;
writer
.write_all(plaintext)
.context("Failed to write encrypted data")?;
writer.finish().context("Failed to finalize encryption")?;
let mut result = b"age-encrypted:v1\n".to_vec();
result.extend(encrypted);
Ok(result)
}
pub fn decrypt(ciphertext: &[u8], passphrase: Option<&String>) -> Result<Vec<u8>> {
let marker = b"age-encrypted:v1\n";
if !ciphertext.starts_with(marker) {
return Ok(ciphertext.to_vec());
}
let actual_ciphertext = &ciphertext[marker.len()..];
let passphrase_str = passphrase
.filter(|p| !p.is_empty())
.context("Profile is encrypted but no passphrase provided")?;
let decryptor =
age::Decryptor::new(actual_ciphertext).context("Failed to parse encrypted data")?;
let mut plaintext = vec![];
decryptor
.decrypt(std::iter::once(
&age::scrypt::Identity::new(secrecy::SecretString::from(passphrase_str.as_str())) as _,
))
.context("Failed to decrypt - wrong passphrase?")?
.read_to_end(&mut plaintext)
.context("Failed to read decrypted data")?;
Ok(plaintext)
}
pub fn is_encrypted(data: &[u8]) -> bool {
data.starts_with(b"age-encrypted:v1\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_passphrase_no_encryption() {
let plaintext = b"hello world";
let encrypted = encrypt(plaintext, None).unwrap();
assert_eq!(encrypted, plaintext);
assert!(!is_encrypted(&encrypted));
}
#[test]
fn test_empty_passphrase_no_encryption() {
let plaintext = b"hello world";
let passphrase = String::new();
let encrypted = encrypt(plaintext, Some(&passphrase)).unwrap();
assert_eq!(encrypted, plaintext);
assert!(!is_encrypted(&encrypted));
}
#[test]
fn test_encrypt_decrypt() {
let plaintext = b"sensitive auth data";
let passphrase = String::from("my secret password");
let encrypted = encrypt(plaintext, Some(&passphrase)).unwrap();
assert!(is_encrypted(&encrypted));
assert_ne!(encrypted, plaintext.to_vec());
let decrypted = decrypt(&encrypted, Some(&passphrase)).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_decrypt_wrong_passphrase() {
let plaintext = b"sensitive auth data";
let passphrase = String::from("correct password");
let wrong_passphrase = String::from("wrong password");
let encrypted = encrypt(plaintext, Some(&passphrase)).unwrap();
assert!(decrypt(&encrypted, Some(&wrong_passphrase)).is_err());
}
#[test]
fn test_backward_compatible_plaintext() {
let plaintext = b"{\"token\": \"abc123\"}";
let decrypted = decrypt(plaintext, None).unwrap();
assert_eq!(decrypted, plaintext);
}
}