1use crate::error::{Error, Result};
13use argon2::{Algorithm, Argon2, Params, Version};
14use chacha20poly1305::aead::{Aead, KeyInit, Payload};
15use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce};
16use serde::{Deserialize, Serialize};
17use serde_bytes::ByteBuf;
18
19pub const LABEL_BLOB: u8 = 1;
20pub const LABEL_INDEX: u8 = 2;
21pub const LABEL_MANIFEST: u8 = 3;
22pub const LABEL_DICT: u8 = 4;
23
24pub fn random_bytes(n: usize) -> Result<Vec<u8>> {
25 let mut v = vec![0u8; n];
26 getrandom::getrandom(&mut v).map_err(|e| Error::Crypto(format!("rng: {e}")))?;
27 Ok(v)
28}
29
30#[derive(Clone)]
31pub struct ArchiveKey(pub [u8; 32]);
32
33impl ArchiveKey {
34 pub fn random() -> Result<Self> {
35 let v = random_bytes(32)?;
36 let mut k = [0u8; 32];
37 k.copy_from_slice(&v);
38 Ok(ArchiveKey(k))
39 }
40
41 fn sub(&self, context: &str) -> [u8; 32] {
42 blake3::derive_key(context, &self.0)
43 }
44
45 pub fn blob_key(&self) -> [u8; 32] {
46 self.sub("tsaur v1 blob key")
47 }
48
49 pub fn nonce_key(&self) -> [u8; 32] {
50 self.sub("tsaur v1 nonce key")
51 }
52}
53
54fn nonce_for(key: &ArchiveKey, label: u8, index: u64) -> [u8; 24] {
55 let mut m = [0u8; 9];
56 m[0] = label;
57 m[1..].copy_from_slice(&index.to_le_bytes());
58 let h = blake3::keyed_hash(&key.nonce_key(), &m);
59 let mut n = [0u8; 24];
60 n.copy_from_slice(&h.as_bytes()[..24]);
61 n
62}
63
64pub fn seal(key: &ArchiveKey, label: u8, index: u64, aad: &[u8], plaintext: &[u8]) -> Result<Vec<u8>> {
65 let cipher = XChaCha20Poly1305::new(Key::from_slice(&key.blob_key()));
66 let nonce = nonce_for(key, label, index);
67 cipher.encrypt(XNonce::from_slice(&nonce), Payload { msg: plaintext, aad }).map_err(|_| Error::Crypto("encryption failed".into()))
68}
69
70pub fn open(key: &ArchiveKey, label: u8, index: u64, aad: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> {
71 let cipher = XChaCha20Poly1305::new(Key::from_slice(&key.blob_key()));
72 let nonce = nonce_for(key, label, index);
73 cipher.decrypt(XNonce::from_slice(&nonce), Payload { msg: ciphertext, aad }).map_err(|_| Error::Crypto("authentication failed (wrong password or corrupt data)".into()))
74}
75
76#[derive(Clone, Copy, Debug)]
77pub struct KdfParams {
78 pub m_kib: u32,
79 pub t: u32,
80 pub p: u32,
81}
82
83impl Default for KdfParams {
84 fn default() -> Self {
86 Self { m_kib: 256 * 1024, t: 3, p: 4 }
87 }
88}
89
90impl KdfParams {
91 pub const FLOOR: KdfParams = KdfParams { m_kib: 64 * 1024, t: 3, p: 1 };
93}
94
95#[derive(Clone, Debug, Serialize, Deserialize)]
101pub struct Stanza {
102 pub t: String,
103 #[serde(default, skip_serializing_if = "bytebuf_is_empty")]
104 pub salt: ByteBuf,
105 #[serde(default, skip_serializing_if = "is_zero")]
106 pub m_kib: u32,
107 #[serde(default, skip_serializing_if = "is_zero")]
108 pub t_cost: u32,
109 #[serde(default, skip_serializing_if = "is_zero")]
110 pub p: u32,
111 #[serde(default, skip_serializing_if = "bytebuf_is_empty")]
112 pub eph: ByteBuf,
113 #[serde(default, skip_serializing_if = "bytebuf_is_empty")]
114 pub ct: ByteBuf,
115 pub wrapped: ByteBuf,
116}
117
118fn bytebuf_is_empty(b: &ByteBuf) -> bool {
119 b.as_ref().is_empty()
120}
121
122fn is_zero(x: &u32) -> bool {
123 *x == 0
124}
125
126#[derive(Clone, Debug, Serialize, Deserialize, Default)]
127pub struct RecipientsBlock {
128 pub stanzas: Vec<Stanza>,
129}
130
131#[derive(Clone, Default)]
133pub struct Credentials {
134 pub password: Option<String>,
135 pub identities: Vec<hybrid::Identity>,
136}
137
138fn argon2id(password: &[u8], salt: &[u8], kp: &KdfParams) -> Result<[u8; 32]> {
139 let params = Params::new(kp.m_kib, kp.t, kp.p, Some(32)).map_err(|e| Error::Crypto(format!("argon2 params: {e}")))?;
140 let a = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
141 let mut out = [0u8; 32];
142 a.hash_password_into(password, salt, &mut out).map_err(|e| Error::Crypto(format!("argon2: {e}")))?;
143 Ok(out)
144}
145
146fn wrap_dek(dek: &ArchiveKey, kek: &[u8; 32]) -> Result<Vec<u8>> {
147 let nonce = random_bytes(24)?;
148 let cipher = XChaCha20Poly1305::new(Key::from_slice(kek));
149 let ct = cipher.encrypt(XNonce::from_slice(&nonce), Payload { msg: &dek.0, aad: b"tsaur v1 dek" }).map_err(|_| Error::Crypto("wrap failed".into()))?;
150 let mut wrapped = nonce;
151 wrapped.extend_from_slice(&ct);
152 Ok(wrapped)
153}
154
155fn unwrap_dek(wrapped: &[u8], kek: &[u8; 32]) -> Result<ArchiveKey> {
156 if wrapped.len() < 24 + 32 + 16 {
157 return Err(Error::Corrupt("recipient stanza too short".into()));
158 }
159 let cipher = XChaCha20Poly1305::new(Key::from_slice(kek));
160 let (nonce, ct) = wrapped.split_at(24);
161 let dek = cipher.decrypt(XNonce::from_slice(nonce), Payload { msg: ct, aad: b"tsaur v1 dek" }).map_err(|_| Error::Crypto("wrong password/identity (or corrupt recipient stanza)".into()))?;
162 let mut k = [0u8; 32];
163 k.copy_from_slice(&dek);
164 Ok(ArchiveKey(k))
165}
166
167pub fn wrap_password(dek: &ArchiveKey, password: &[u8], kp: &KdfParams) -> Result<Stanza> {
168 if kp.m_kib < KdfParams::FLOOR.m_kib || kp.t < KdfParams::FLOOR.t || kp.p < 1 {
169 return Err(Error::Invalid("KDF parameters below the allowed floor (64 MiB, t=3)".into()));
170 }
171 let salt = random_bytes(16)?;
172 let kek = argon2id(password, &salt, kp)?;
173 let wrapped = wrap_dek(dek, &kek)?;
174 Ok(Stanza { t: "argon2id".into(), salt: ByteBuf::from(salt), m_kib: kp.m_kib, t_cost: kp.t, p: kp.p, eph: ByteBuf::new(), ct: ByteBuf::new(), wrapped: ByteBuf::from(wrapped) })
175}
176
177pub fn unwrap_password(stanza: &Stanza, password: &[u8]) -> Result<ArchiveKey> {
178 if stanza.m_kib < KdfParams::FLOOR.m_kib || stanza.t_cost < KdfParams::FLOOR.t || stanza.p < 1 {
179 return Err(Error::Policy("archive KDF parameters are below the allowed floor".into()));
180 }
181 let kp = KdfParams { m_kib: stanza.m_kib, t: stanza.t_cost, p: stanza.p };
182 let kek = argon2id(password, &stanza.salt, &kp)?;
183 unwrap_dek(&stanza.wrapped, &kek)
184}
185
186pub fn unlock(block: &RecipientsBlock, creds: &Credentials) -> Result<ArchiveKey> {
188 let mut last = Error::Crypto("archive is encrypted: no matching password or identity".into());
189 for s in &block.stanzas {
190 match s.t.as_str() {
191 "argon2id" => {
192 if let Some(pw) = &creds.password {
193 match unwrap_password(s, pw.as_bytes()) {
194 Ok(k) => return Ok(k),
195 Err(e) => last = e,
196 }
197 }
198 }
199 "x25519mlkem768" => {
200 for id in &creds.identities {
201 match hybrid::unwrap(s, id) {
202 Ok(k) => return Ok(k),
203 Err(e) => last = e,
204 }
205 }
206 }
207 _ => {}
208 }
209 }
210 Err(last)
211}
212
213pub mod hybrid {
218 use super::*;
219 use hkdf::Hkdf;
220 use kem::{Decapsulate, Encapsulate, FromSeed, KeyExport};
221 use ml_kem::MlKem768;
222 use sha2::Sha256;
223 use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret};
224
225 pub const IDENTITY_LEN: usize = 32 + 64;
226 pub const RECIPIENT_LEN: usize = 32 + 1184;
227 pub const CT_LEN: usize = 1088;
228
229 #[derive(Clone)]
231 pub struct Identity {
232 x_secret: [u8; 32],
233 k_seed: [u8; 64],
234 }
235
236 #[derive(Clone, Debug, PartialEq, Eq)]
238 pub struct Recipient {
239 pub x_pub: [u8; 32],
240 pub k_ek: Vec<u8>,
241 }
242
243 impl Identity {
244 pub fn generate() -> Result<Self> {
245 let x = random_bytes(32)?;
246 let s = random_bytes(64)?;
247 let mut x_secret = [0u8; 32];
248 x_secret.copy_from_slice(&x);
249 let mut k_seed = [0u8; 64];
250 k_seed.copy_from_slice(&s);
251 Ok(Self { x_secret, k_seed })
252 }
253
254 pub fn to_bytes(&self) -> Vec<u8> {
255 let mut v = self.x_secret.to_vec();
256 v.extend_from_slice(&self.k_seed);
257 v
258 }
259
260 pub fn from_bytes(b: &[u8]) -> Result<Self> {
261 if b.len() != IDENTITY_LEN {
262 return Err(Error::Invalid(format!("identity must be {IDENTITY_LEN} bytes")));
263 }
264 let mut x_secret = [0u8; 32];
265 x_secret.copy_from_slice(&b[..32]);
266 let mut k_seed = [0u8; 64];
267 k_seed.copy_from_slice(&b[32..]);
268 Ok(Self { x_secret, k_seed })
269 }
270
271 pub fn recipient(&self) -> Recipient {
272 let x_pub = PublicKey::from(&StaticSecret::from(self.x_secret)).to_bytes();
273 let seed = kem::Seed::<MlKem768>::from(self.k_seed);
274 let (_dk, ek) = MlKem768::from_seed(&seed);
275 Recipient { x_pub, k_ek: ek.to_bytes().to_vec() }
276 }
277 }
278
279 impl Recipient {
280 pub fn to_bytes(&self) -> Vec<u8> {
281 let mut v = self.x_pub.to_vec();
282 v.extend_from_slice(&self.k_ek);
283 v
284 }
285
286 pub fn from_bytes(b: &[u8]) -> Result<Self> {
287 if b.len() != RECIPIENT_LEN {
288 return Err(Error::Invalid(format!("recipient key must be {RECIPIENT_LEN} bytes")));
289 }
290 let mut x_pub = [0u8; 32];
291 x_pub.copy_from_slice(&b[..32]);
292 Ok(Self { x_pub, k_ek: b[32..].to_vec() })
293 }
294 }
295
296 fn kek(ss_kem: &[u8], ss_x: &[u8], eph_pub: &[u8; 32], ct: &[u8]) -> [u8; 32] {
297 let mut ikm = ss_kem.to_vec();
298 ikm.extend_from_slice(ss_x);
299 let mut salt = eph_pub.to_vec();
300 salt.extend_from_slice(blake3::hash(ct).as_bytes());
301 let hk = Hkdf::<Sha256>::new(Some(&salt), &ikm);
302 let mut out = [0u8; 32];
303 hk.expand(b"tsaur v1 x25519mlkem768", &mut out).expect("hkdf output length");
304 out
305 }
306
307 pub fn wrap(dek: &ArchiveKey, r: &Recipient) -> Result<Stanza> {
308 let ek_arr = kem::Key::<ml_kem::EncapsulationKey<MlKem768>>::try_from(&r.k_ek[..]).map_err(|_| Error::Invalid("bad ML-KEM encapsulation key length".into()))?;
309 let ek = ml_kem::EncapsulationKey::<MlKem768>::new(&ek_arr).map_err(|_| Error::Invalid("invalid ML-KEM encapsulation key".into()))?;
310 let (ct, ss_kem) = ek.encapsulate();
311 let eph = EphemeralSecret::random();
312 let eph_pub = PublicKey::from(&eph).to_bytes();
313 let ss_x = eph.diffie_hellman(&PublicKey::from(r.x_pub));
314 let k = kek(ss_kem.as_slice(), ss_x.as_bytes(), &eph_pub, ct.as_slice());
315 Ok(Stanza {
316 t: "x25519mlkem768".into(),
317 salt: ByteBuf::new(),
318 m_kib: 0,
319 t_cost: 0,
320 p: 0,
321 eph: ByteBuf::from(eph_pub.to_vec()),
322 ct: ByteBuf::from(ct.as_slice().to_vec()),
323 wrapped: ByteBuf::from(wrap_dek(dek, &k)?),
324 })
325 }
326
327 pub fn unwrap(stanza: &Stanza, id: &Identity) -> Result<ArchiveKey> {
328 if stanza.eph.len() != 32 || stanza.ct.len() != CT_LEN {
329 return Err(Error::Corrupt("malformed hybrid stanza".into()));
330 }
331 let seed = kem::Seed::<MlKem768>::from(id.k_seed);
332 let (dk, _ek) = MlKem768::from_seed(&seed);
333 let ct = kem::Ciphertext::<MlKem768>::try_from(&stanza.ct[..]).map_err(|_| Error::Corrupt("bad ML-KEM ciphertext length".into()))?;
334 let ss_kem = dk.decapsulate(&ct);
335 let mut eph_pub = [0u8; 32];
336 eph_pub.copy_from_slice(&stanza.eph);
337 let ss_x = StaticSecret::from(id.x_secret).diffie_hellman(&PublicKey::from(eph_pub));
338 let k = kek(ss_kem.as_slice(), ss_x.as_bytes(), &eph_pub, &stanza.ct);
339 unwrap_dek(&stanza.wrapped, &k)
340 }
341}
342
343pub mod sig {
345 use super::*;
346 use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
347
348 pub fn keygen() -> Result<([u8; 32], [u8; 32])> {
350 let v = random_bytes(32)?;
351 let mut seed = [0u8; 32];
352 seed.copy_from_slice(&v);
353 let sk = SigningKey::from_bytes(&seed);
354 Ok((seed, sk.verifying_key().to_bytes()))
355 }
356
357 pub fn public_key(seed: &[u8; 32]) -> [u8; 32] {
358 SigningKey::from_bytes(seed).verifying_key().to_bytes()
359 }
360
361 pub fn sign(seed: &[u8; 32], msg: &[u8]) -> [u8; 64] {
362 SigningKey::from_bytes(seed).sign(msg).to_bytes()
363 }
364
365 pub fn verify(pubkey: &[u8; 32], msg: &[u8], sig: &[u8]) -> bool {
366 let Ok(vk) = VerifyingKey::from_bytes(pubkey) else { return false };
367 let Ok(sig_arr): std::result::Result<[u8; 64], _> = sig.try_into() else { return false };
368 let s = Signature::from_bytes(&sig_arr);
369 vk.verify_strict(msg, &s).is_ok()
370 }
371}