age_plugin_argon2/
recipient.rs1use std::collections::HashSet;
2
3use age::EncryptError;
4use age_core::format::{FileKey, Stanza};
5use argon2::{Algorithm, Argon2, Version};
6use base64::engine::general_purpose::STANDARD_NO_PAD;
7use base64::Engine;
8use rand::rngs::OsRng;
9use rand::RngCore;
10use secrecy::ExposeSecret;
11use uuid::Uuid;
12
13use crate::params::Argon2Params;
14
15const STANZA_TAG: &str = "thesis.co/argon2";
16
17pub struct Argon2idRecipient {
23 passphrase: Vec<u8>,
24 params: Argon2Params,
25}
26
27impl Argon2idRecipient {
28 pub fn new(passphrase: &[u8], params: Argon2Params) -> Self {
30 Self {
31 passphrase: passphrase.to_vec(),
32 params,
33 }
34 }
35}
36
37impl age::Recipient for Argon2idRecipient {
38 fn wrap_file_key(
39 &self,
40 file_key: &FileKey,
41 ) -> Result<(Vec<Stanza>, HashSet<String>), EncryptError> {
42 let mut salt = [0u8; 16];
44 OsRng.fill_bytes(&mut salt);
45
46 let wrapping_key = derive_wrapping_key(&self.passphrase, &salt, &self.params)?;
48
49 let body = age_core::primitives::aead_encrypt(&wrapping_key, file_key.expose_secret());
51
52 let stanza = Stanza {
54 tag: STANZA_TAG.to_string(),
55 args: vec![
56 STANDARD_NO_PAD.encode(salt),
57 self.params.m_cost().to_string(),
58 self.params.t_cost().to_string(),
59 self.params.p_cost().to_string(),
60 ],
61 body,
62 };
63
64 let mut labels = HashSet::new();
66 labels.insert(Uuid::new_v4().to_string());
67
68 Ok((vec![stanza], labels))
69 }
70}
71
72pub(crate) fn derive_wrapping_key(
76 passphrase: &[u8],
77 salt: &[u8],
78 params: &Argon2Params,
79) -> Result<[u8; 32], age::EncryptError> {
80 let argon2_params =
81 argon2::Params::new(params.m_cost(), params.t_cost(), params.p_cost(), Some(32)).map_err(
82 |e| age::EncryptError::Io(std::io::Error::new(std::io::ErrorKind::InvalidInput, e)),
83 )?;
84
85 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, argon2_params);
86
87 let mut key = [0u8; 32];
88 argon2
89 .hash_password_into(passphrase, salt, &mut key)
90 .map_err(|e| {
91 age::EncryptError::Io(std::io::Error::new(std::io::ErrorKind::InvalidInput, e))
92 })?;
93
94 Ok(key)
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use age::Recipient;
101
102 #[test]
103 fn test_derive_wrapping_key_deterministic() {
104 let passphrase = b"test-password";
105 let salt = [1u8; 16];
106 let params = Argon2Params::new(256, 1, 1).unwrap();
107
108 let key1 = derive_wrapping_key(passphrase, &salt, ¶ms).unwrap();
109 let key2 = derive_wrapping_key(passphrase, &salt, ¶ms).unwrap();
110 assert_eq!(key1, key2);
111 }
112
113 #[test]
114 fn test_wrap_file_key_produces_valid_stanza() {
115 let recipient = Argon2idRecipient::new(b"test", Argon2Params::new(256, 1, 1).unwrap());
116
117 let file_key = FileKey::new(Box::new([42u8; 16]));
118 let (stanzas, labels) = recipient.wrap_file_key(&file_key).unwrap();
119
120 assert_eq!(stanzas.len(), 1);
121 assert_eq!(stanzas[0].tag, "thesis.co/argon2");
122 assert_eq!(stanzas[0].args.len(), 4);
123 assert_eq!(stanzas[0].body.len(), 32);
125 assert_eq!(labels.len(), 1);
127 }
128}