use rand::rngs::OsRng;
use std::fmt;
use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret};
use anubis_core::{
format::{FileKey, Stanza, FILE_KEY_BYTES},
primitives::{aead_decrypt, aead_encrypt, hkdf},
secrecy::ExposeSecret,
};
use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
use bech32::{ToBase32, Variant};
use zeroize::{Zeroize, Zeroizing};
use crate::{
error::{DecryptError, EncryptError},
util::read::base64_arg,
};
const X25519_EPK_LABEL: &str = "X25519";
const X25519_RECIPIENT_TAG: &str = "X25519";
pub struct Identity(StaticSecret);
impl Identity {
pub fn generate() -> Self {
let mut rng = OsRng;
Identity(StaticSecret::random_from_rng(&mut rng))
}
pub fn to_public(&self) -> Recipient {
Recipient(PublicKey::from(&self.0))
}
pub(crate) fn diffie_hellman(&self, epk: &[u8; 32]) -> Result<[u8; 32], DecryptError> {
let epk = PublicKey::from(*epk);
let shared_secret = self.0.diffie_hellman(&epk);
Ok(*shared_secret.as_bytes())
}
fn unwrap_stanza(&self, stanza: &Stanza) -> Option<Result<FileKey, DecryptError>> {
if stanza.tag != X25519_RECIPIENT_TAG {
return None;
}
if stanza.args.len() != 1 {
return Some(Err(DecryptError::InvalidHeader));
}
const ENCRYPTED_FILE_KEY_BYTES: usize = FILE_KEY_BYTES + 16;
if stanza.body.len() != ENCRYPTED_FILE_KEY_BYTES {
return Some(Err(DecryptError::InvalidHeader));
}
let epk_bytes = match base64_arg::<_, 32, 64>(&stanza.args[0]) {
Some(bytes) => bytes,
None => return Some(Err(DecryptError::InvalidHeader)),
};
let epk = PublicKey::from(epk_bytes);
let shared_secret = self.0.diffie_hellman(&epk);
let mut salt = Vec::with_capacity(64);
salt.extend_from_slice(PublicKey::from(&self.0).as_bytes());
salt.extend_from_slice(&epk_bytes);
let wrap_key = hkdf(
&salt,
b"age-encryption.org/v1/X25519",
shared_secret.as_bytes(),
);
aead_decrypt(&Zeroizing::new(wrap_key), FILE_KEY_BYTES, &stanza.body)
.ok()
.map(|mut plaintext| {
Ok(FileKey::init_with_mut(|file_key| {
file_key.copy_from_slice(&plaintext);
plaintext.zeroize();
}))
})
}
}
impl crate::Identity for Identity {
fn unwrap_stanza(&self, stanza: &Stanza) -> Option<Result<FileKey, DecryptError>> {
Identity::unwrap_stanza(self, stanza)
}
}
#[derive(Clone)]
pub struct Recipient(PublicKey);
impl Recipient {
pub(crate) fn public_key(&self) -> &PublicKey {
&self.0
}
fn wrap_file_key(&self, file_key: &FileKey) -> Result<Vec<Stanza>, EncryptError> {
let mut rng = OsRng;
let esk = EphemeralSecret::random_from_rng(&mut rng);
let epk = PublicKey::from(&esk);
let shared_secret = esk.diffie_hellman(&self.0);
let mut salt = Vec::with_capacity(64);
salt.extend_from_slice(self.0.as_bytes());
salt.extend_from_slice(epk.as_bytes());
let wrap_key = hkdf(
&salt,
b"age-encryption.org/v1/X25519",
shared_secret.as_bytes(),
);
let encrypted_file_key = aead_encrypt(&Zeroizing::new(wrap_key), file_key.expose_secret());
Ok(vec![Stanza {
tag: X25519_RECIPIENT_TAG.to_string(),
args: vec![BASE64_STANDARD_NO_PAD.encode(epk.as_bytes())],
body: encrypted_file_key,
}])
}
}
impl crate::Recipient for Recipient {
fn wrap_file_key(
&self,
file_key: &FileKey,
) -> Result<(Vec<Stanza>, std::collections::HashSet<String>), EncryptError> {
Ok((self.wrap_file_key(file_key)?, std::collections::HashSet::new()))
}
}
impl fmt::Display for Recipient {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"age1{}",
bech32::encode(
"x25519",
self.0.as_bytes().to_base32(),
Variant::Bech32
)
.map_err(|_| fmt::Error)?
)
}
}
impl fmt::Display for Identity {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"AGE-SECRET-KEY-1{}",
bech32::encode(
"",
self.0.to_bytes().to_base32(),
Variant::Bech32
)
.map_err(|_| fmt::Error)?
.to_uppercase()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn x25519_round_trip() {
let identity = Identity::generate();
let recipient = identity.to_public();
let file_key = FileKey::new(Box::new([42; 16]));
let stanzas = recipient.wrap_file_key(&file_key).unwrap();
assert_eq!(stanzas.len(), 1);
assert_eq!(stanzas[0].tag, X25519_RECIPIENT_TAG);
let decrypted = identity.unwrap_stanza(&stanzas[0]).unwrap().unwrap();
assert_eq!(decrypted.expose_secret(), file_key.expose_secret());
}
#[test]
fn x25519_public_key_encoding() {
let identity = Identity::generate();
let recipient = identity.to_public();
let encoded = recipient.to_string();
assert!(encoded.starts_with("age1"));
assert!(encoded.len() > 10);
}
}