use std::io::{Read, Write};
use std::str::FromStr;
use age::x25519::{Identity, Recipient};
use secrecy::ExposeSecret;
use crate::error::{DotlingError, Result};
pub fn encrypt(plaintext: &[u8], recipient_strings: &[String]) -> Result<Vec<u8>> {
let mut recipients: Vec<Box<dyn age::Recipient + Send>> = Vec::new();
for r in recipient_strings {
let recipient = Recipient::from_str(r).map_err(|e| {
DotlingError::Crypto(format!("Invalid recipient '{r}': {e}"))
})?;
recipients.push(Box::new(recipient));
}
if recipients.is_empty() {
return Err(DotlingError::Crypto(
"No encryption recipients configured in .dotling.toml".to_string(),
));
}
let encryptor = age::Encryptor::with_recipients(recipients.iter().map(|r| r.as_ref() as &dyn age::Recipient))
.map_err(|e| DotlingError::Crypto(format!("Failed to create encryptor: {e}")))?;
let mut encrypted = Vec::new();
{
let mut writer = encryptor
.wrap_output(&mut encrypted)
.map_err(|e| DotlingError::Crypto(format!("Encryption failed: {e}")))?;
writer
.write_all(plaintext)
.map_err(|e| DotlingError::Crypto(format!("Failed to write encrypted data: {e}")))?;
writer
.finish()
.map_err(|e| DotlingError::Crypto(format!("Failed to finish encryption: {e}")))?;
}
Ok(encrypted)
}
pub fn decrypt(ciphertext: &[u8], identity_string: &str) -> Result<Vec<u8>> {
let identity = Identity::from_str(identity_string).map_err(|e| {
DotlingError::Crypto(format!("Invalid identity key: {e}"))
})?;
let decryptor = age::Decryptor::new(ciphertext)
.map_err(|e| DotlingError::Crypto(format!("Decryption failed: {e}")))?;
let mut reader = decryptor
.decrypt(std::iter::once(&identity as &dyn age::Identity))
.map_err(|e| DotlingError::Crypto(format!("Failed to decrypt: {e}")))?;
let mut plaintext = Vec::new();
reader
.read_to_end(&mut plaintext)
.map_err(|e| DotlingError::Crypto(format!("Failed to read plaintext: {e}")))?;
Ok(plaintext)
}
pub fn generate_keypair() -> (String, String) {
let identity = Identity::generate();
let public = identity.to_public().to_string();
let secret = identity.to_string().expose_secret().to_string();
(public, secret)
}
pub fn get_default_identity() -> Result<String> {
if let Some(config_dir) = dirs::config_dir() {
let identity_file = config_dir.join("dotling").join("identity.txt");
if identity_file.exists() {
let content = std::fs::read_to_string(&identity_file)
.map_err(crate::error::io_err(&identity_file))?;
return Ok(content);
}
}
Err(DotlingError::Crypto(
"No identity file found at ~/.config/dotling/identity.txt. Run `dotling keygen --save` first.".to_string()
))
}