1use crate::check::Check;
10use crate::container::ChunkType;
11use crate::error::{Error, Result};
12use crate::io::{Reader, Writer};
13
14use aes_gcm::aead::generic_array::GenericArray;
15use aes_gcm::aead::{Aead, KeyInit, Payload};
16use aes_gcm::Aes256Gcm;
17use aes_kw::KekAes256;
18use argon2::{Algorithm, Argon2, Params, Version};
19use chacha20poly1305::ChaCha20Poly1305;
20use hkdf::Hkdf;
21use rand::rngs::OsRng;
22use rand::RngCore;
23use sha2::{Digest, Sha256};
24use x25519_dalek::{PublicKey, StaticSecret};
25
26const AUTH_VERSION: u32 = 1;
28const AUTH_HEADER_LEN: usize = 20;
30
31pub const CIPHER_A256: [u8; 4] = *b"A256";
33pub const CIPHER_C20P: [u8; 4] = *b"C20P";
35
36pub const MODE_PASSWORD: u32 = 1;
38pub const MODE_MACHINE: u32 = 1 << 1;
40
41pub const PASSWORD_SECTION_LEN: usize = 65;
43pub const RECIPIENT_ENTRY_LEN: usize = 104;
45pub const UNIT_OVERHEAD: usize = 28;
47
48pub const ARGON2_MAX_ITERATIONS: u32 = 10;
50pub const ARGON2_MAX_MEMORY_KIB: u32 = 4_194_304;
52pub const ARGON2_MAX_PARALLELISM: u32 = 16;
54
55pub const MACHINE_BINDING_LABEL: &[u8] = b"LUMEN machine-binding v1\0";
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum Cipher {
61 Aes256Gcm,
63 ChaCha20Poly1305,
65}
66
67impl Cipher {
68 pub fn id(self) -> [u8; 4] {
70 match self {
71 Cipher::Aes256Gcm => CIPHER_A256,
72 Cipher::ChaCha20Poly1305 => CIPHER_C20P,
73 }
74 }
75
76 pub fn from_id(id: [u8; 4]) -> Result<Cipher> {
78 match id {
79 CIPHER_A256 => Ok(Cipher::Aes256Gcm),
80 CIPHER_C20P => Ok(Cipher::ChaCha20Poly1305),
81 _ => Err(Error::new(
82 Check::AuthCipherKnown,
83 format!("unknown cipher_id {}", ChunkType(id).tag()),
84 )),
85 }
86 }
87
88 pub const fn nonce_len(self) -> usize {
90 12
91 }
92
93 pub const fn tag_len(self) -> usize {
95 16
96 }
97}
98
99#[derive(Clone, Copy, PartialEq, Eq)]
101pub struct SessionKey([u8; 32]);
102
103impl SessionKey {
104 pub fn from_bytes(bytes: [u8; 32]) -> SessionKey {
106 SessionKey(bytes)
107 }
108
109 pub fn as_bytes(&self) -> &[u8; 32] {
111 &self.0
112 }
113
114 pub fn generate() -> SessionKey {
116 let mut bytes = [0u8; 32];
117 let mut rng = OsRng;
118 rng.fill_bytes(&mut bytes);
119 SessionKey(bytes)
120 }
121
122 pub fn from_password(auth: &Auth, password: &str) -> Result<SessionKey> {
126 unwrap_password(auth, password)
127 }
128}
129
130impl core::fmt::Debug for SessionKey {
131 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
132 f.write_str("SessionKey(<redacted>)")
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub struct PasswordSection {
139 pub salt: [u8; 16],
141 pub iterations: u32,
143 pub memory_kib: u32,
145 pub parallelism: u8,
147 pub wrapped_key: [u8; 40],
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub struct RecipientEntry {
154 pub machine_fp: [u8; 32],
156 pub ephemeral_pk: [u8; 32],
158 pub wrapped_key: [u8; 40],
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct Auth {
165 pub cipher: Cipher,
167 pub auth_version: u32,
169 pub mode: u32,
171 pub password: Option<PasswordSection>,
173 pub recipients: Vec<RecipientEntry>,
175}
176
177impl Auth {
178 pub fn cipher_id(&self) -> [u8; 4] {
180 self.cipher.id()
181 }
182
183 pub fn has_password(&self) -> bool {
185 self.mode & MODE_PASSWORD != 0
186 }
187
188 pub fn has_machine(&self) -> bool {
190 self.mode & MODE_MACHINE != 0
191 }
192}
193
194pub fn parse_auth(payload: &[u8]) -> Result<Auth> {
200 if payload.len() < AUTH_HEADER_LEN {
201 return Err(Error::new(
202 Check::AuthFrame,
203 format!(
204 "AUTH payload is {} bytes, shorter than the {AUTH_HEADER_LEN}-byte header",
205 payload.len()
206 ),
207 ));
208 }
209
210 let mut r = Reader::checked(payload, Check::AuthFrame);
211 let cipher = Cipher::from_id(r.array::<4>()?)?;
212 let auth_version = r.u32()?;
213 if auth_version != AUTH_VERSION {
214 return Err(Error::new(
215 Check::AuthVersion,
216 format!("unsupported AUTH.auth_version {auth_version}, expected {AUTH_VERSION}"),
217 ));
218 }
219 let mode = r.u32()?;
220 let password_section_len = u64::from(r.u32()?);
221 let machine_section_len = u64::from(r.u32()?);
222
223 if AUTH_HEADER_LEN as u64 + password_section_len + machine_section_len > payload.len() as u64 {
226 return Err(Error::new(
227 Check::AuthFrame,
228 format!(
229 "AUTH declares {password_section_len} password and {machine_section_len} machine bytes but carries only {} bytes after the header",
230 payload.len() - AUTH_HEADER_LEN
231 ),
232 ));
233 }
234 if mode == 0 {
235 return Err(Error::new(
236 Check::CryptModeEmpty,
237 "AUTH.mode is 0: neither a password nor a machine binding is declared",
238 ));
239 }
240
241 let password = if mode & MODE_PASSWORD != 0 {
242 if password_section_len < PASSWORD_SECTION_LEN as u64 {
243 return Err(Error::new(
244 Check::CryptPasswordSectionLen,
245 format!(
246 "AUTH declares a {password_section_len}-byte password section, shorter than the {PASSWORD_SECTION_LEN}-byte fixed section"
247 ),
248 ));
249 }
250 let section = r.bytes(password_section_len as usize)?;
251 Some(read_password_section(section)?)
252 } else {
253 None
254 };
255
256 let recipients = if mode & MODE_MACHINE != 0 {
257 if machine_section_len < RECIPIENT_ENTRY_LEN as u64
258 || machine_section_len % RECIPIENT_ENTRY_LEN as u64 != 0
259 {
260 return Err(Error::new(
261 Check::CryptMachineSectionLen,
262 format!(
263 "AUTH declares a {machine_section_len}-byte machine section, which is not a positive multiple of the {RECIPIENT_ENTRY_LEN}-byte entry size"
264 ),
265 ));
266 }
267 let section = r.bytes(machine_section_len as usize)?;
268 let mut recipients = Vec::with_capacity(section.len() / RECIPIENT_ENTRY_LEN);
269 for entry in section.chunks_exact(RECIPIENT_ENTRY_LEN) {
270 recipients.push(read_recipient_entry(entry)?);
271 }
272 recipients
273 } else {
274 Vec::new()
275 };
276
277 Ok(Auth {
278 cipher,
279 auth_version,
280 mode,
281 password,
282 recipients,
283 })
284}
285
286pub fn encode_auth(auth: &Auth) -> Vec<u8> {
288 let password_section_len = if auth.password.is_some() {
289 PASSWORD_SECTION_LEN
290 } else {
291 0
292 };
293 let machine_section_len = auth.recipients.len().saturating_mul(RECIPIENT_ENTRY_LEN);
294 let capacity = AUTH_HEADER_LEN
295 .saturating_add(password_section_len)
296 .saturating_add(machine_section_len);
297
298 let mut w = Writer::with_capacity(capacity);
299 w.bytes(&auth.cipher.id());
300 w.u32(auth.auth_version);
301 w.u32(auth.mode);
302 w.u32(password_section_len.min(u32::MAX as usize) as u32);
303 w.u32(machine_section_len.min(u32::MAX as usize) as u32);
304 if let Some(section) = auth.password {
305 w.bytes(§ion.salt);
306 w.u32(section.iterations);
307 w.u32(section.memory_kib);
308 w.u8(section.parallelism);
309 w.bytes(§ion.wrapped_key);
310 }
311 for entry in &auth.recipients {
312 w.bytes(&entry.machine_fp);
313 w.bytes(&entry.ephemeral_pk);
314 w.bytes(&entry.wrapped_key);
315 }
316 w.into_vec()
317}
318
319pub fn check_argon2_budget(section: &PasswordSection) -> Result<()> {
321 if section.iterations > ARGON2_MAX_ITERATIONS {
322 return Err(Error::new(
323 Check::CryptArgon2Budget,
324 format!(
325 "AUTH declares {} Argon2id iterations, above the ceiling of {ARGON2_MAX_ITERATIONS}",
326 section.iterations
327 ),
328 ));
329 }
330 if section.memory_kib > ARGON2_MAX_MEMORY_KIB {
331 return Err(Error::new(
332 Check::CryptArgon2Budget,
333 format!(
334 "AUTH declares {} KiB of Argon2id memory, above the ceiling of {ARGON2_MAX_MEMORY_KIB}",
335 section.memory_kib
336 ),
337 ));
338 }
339 if u32::from(section.parallelism) > ARGON2_MAX_PARALLELISM {
340 return Err(Error::new(
341 Check::CryptArgon2Budget,
342 format!(
343 "AUTH declares {} Argon2id lanes, above the ceiling of {ARGON2_MAX_PARALLELISM}",
344 section.parallelism
345 ),
346 ));
347 }
348 Ok(())
349}
350
351pub fn associated_data(chunk_type: ChunkType, unit_index: u32) -> [u8; 9] {
359 let mut aad = [0u8; 9];
360 aad[..4].copy_from_slice(&chunk_type.0);
361 aad[4] = 0;
362 aad[5..].copy_from_slice(&unit_index.to_le_bytes());
363 aad
364}
365
366pub fn seal(
370 cipher: Cipher,
371 key: &SessionKey,
372 chunk_type: ChunkType,
373 unit_index: u32,
374 plaintext: &[u8],
375) -> Result<Vec<u8>> {
376 let mut nonce = [0u8; 12];
377 let mut rng = OsRng;
378 rng.fill_bytes(&mut nonce);
379
380 let aad = associated_data(chunk_type, unit_index);
381 let body = aead_encrypt(cipher, key.as_bytes(), &nonce, plaintext, &aad)?;
382
383 let mut out = Vec::with_capacity(nonce.len() + body.len());
384 out.extend_from_slice(&nonce);
385 out.extend_from_slice(&body);
386 Ok(out)
387}
388
389pub fn open(
391 cipher: Cipher,
392 key: &SessionKey,
393 chunk_type: ChunkType,
394 unit_index: u32,
395 sealed: &[u8],
396) -> Result<Vec<u8>> {
397 if sealed.len() < UNIT_OVERHEAD {
398 return Err(Error::new(
399 Check::CryptTagVerify,
400 format!(
401 "sealed unit is {} bytes, shorter than the {UNIT_OVERHEAD}-byte nonce and tag framing",
402 sealed.len()
403 ),
404 ));
405 }
406
407 let (nonce, body) = sealed.split_at(cipher.nonce_len());
410 let aad = associated_data(chunk_type, unit_index);
411 aead_decrypt(cipher, key.as_bytes(), nonce, body, &aad)
412}
413
414pub fn machine_fingerprint(public_key: &[u8; 32]) -> [u8; 32] {
416 let digest = Sha256::digest(public_key);
417 let mut out = [0u8; 32];
418 out.copy_from_slice(&digest);
419 out
420}
421
422pub fn unwrap_password(auth: &Auth, password: &str) -> Result<SessionKey> {
424 let Some(section) = auth.password.as_ref() else {
425 return Err(Error::new(
426 Check::CryptNoKey,
427 "AUTH carries no password section",
428 ));
429 };
430 check_argon2_budget(section)?;
431 let kek = password_kek(
432 password,
433 §ion.salt,
434 section.iterations,
435 section.memory_kib,
436 section.parallelism,
437 )?;
438 Ok(SessionKey::from_bytes(aes_kw_unwrap(
439 &kek,
440 §ion.wrapped_key,
441 )?))
442}
443
444pub fn unwrap_machine(auth: &Auth, private_key: &[u8; 32]) -> Result<SessionKey> {
449 let secret = StaticSecret::from(*private_key);
450 let fingerprint = machine_fingerprint(PublicKey::from(&secret).as_bytes());
451
452 let mut matched = false;
453 for entry in &auth.recipients {
454 if entry.machine_fp != fingerprint {
455 continue;
456 }
457 matched = true;
458
459 let shared = secret.diffie_hellman(&PublicKey::from(entry.ephemeral_pk));
460 let ss = shared.as_bytes();
461 if ss.iter().all(|byte| *byte == 0) {
462 continue;
465 }
466
467 let kek = machine_kek(ss, &entry.ephemeral_pk, &entry.machine_fp)?;
470 return Ok(SessionKey::from_bytes(aes_kw_unwrap(
471 &kek,
472 &entry.wrapped_key,
473 )?));
474 }
475
476 if matched {
477 Err(Error::new(
478 Check::CryptLowOrderPoint,
479 "every recipient entry matching this machine key yields the all-zero X25519 shared secret",
480 ))
481 } else {
482 Err(Error::new(
483 Check::CryptNoKey,
484 "no recipient entry carries the fingerprint of this machine key",
485 ))
486 }
487}
488
489pub fn wrap_password(
491 session: &SessionKey,
492 password: &str,
493 iterations: u32,
494 memory_kib: u32,
495 parallelism: u8,
496) -> Result<PasswordSection> {
497 let mut salt = [0u8; 16];
498 let mut rng = OsRng;
499 rng.fill_bytes(&mut salt);
500
501 let kek = password_kek(password, &salt, iterations, memory_kib, parallelism)?;
502 Ok(PasswordSection {
503 salt,
504 iterations,
505 memory_kib,
506 parallelism,
507 wrapped_key: aes_kw_wrap(&kek, session.as_bytes())?,
508 })
509}
510
511pub fn wrap_machine(session: &SessionKey, recipient_public: &[u8; 32]) -> Result<RecipientEntry> {
513 let mut ephemeral = [0u8; 32];
514 let mut rng = OsRng;
515 rng.fill_bytes(&mut ephemeral);
516
517 let secret = StaticSecret::from(ephemeral);
518 let ephemeral_public = PublicKey::from(&secret);
519 let shared = secret.diffie_hellman(&PublicKey::from(*recipient_public));
520
521 let fingerprint = machine_fingerprint(recipient_public);
522 let kek = machine_kek(shared.as_bytes(), ephemeral_public.as_bytes(), &fingerprint)?;
523 Ok(RecipientEntry {
524 machine_fp: fingerprint,
525 ephemeral_pk: *ephemeral_public.as_bytes(),
526 wrapped_key: aes_kw_wrap(&kek, session.as_bytes())?,
527 })
528}
529
530fn read_password_section(section: &[u8]) -> Result<PasswordSection> {
535 let mut r = Reader::checked(section, Check::CryptPasswordSectionLen);
536 Ok(PasswordSection {
537 salt: r.array::<16>()?,
538 iterations: r.u32()?,
539 memory_kib: r.u32()?,
540 parallelism: r.u8()?,
541 wrapped_key: r.array::<40>()?,
542 })
543}
544
545fn read_recipient_entry(entry: &[u8]) -> Result<RecipientEntry> {
547 let mut r = Reader::checked(entry, Check::CryptRecipientEntry);
548 Ok(RecipientEntry {
549 machine_fp: r.array::<32>()?,
550 ephemeral_pk: r.array::<32>()?,
551 wrapped_key: r.array::<40>()?,
552 })
553}
554
555fn password_kek(
557 password: &str,
558 salt: &[u8; 16],
559 iterations: u32,
560 memory_kib: u32,
561 parallelism: u8,
562) -> Result<[u8; 32]> {
563 let params =
564 Params::new(memory_kib, iterations, u32::from(parallelism), Some(32)).map_err(|e| {
565 Error::new(
566 Check::CryptArgon2Budget,
567 format!("unsupported Argon2id parameters: {e}"),
568 )
569 })?;
570 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
571
572 let mut kek = [0u8; 32];
573 argon2
574 .hash_password_into(password.as_bytes(), salt, &mut kek)
575 .map_err(|e| {
576 Error::new(
577 Check::CryptArgon2Budget,
578 format!("Argon2id derivation failed: {e}"),
579 )
580 })?;
581 Ok(kek)
582}
583
584fn machine_kek(ss: &[u8; 32], ephemeral_pk: &[u8; 32], machine_fp: &[u8; 32]) -> Result<[u8; 32]> {
587 let mut info = [0u8; MACHINE_BINDING_LABEL.len() + 64];
588 info[..MACHINE_BINDING_LABEL.len()].copy_from_slice(MACHINE_BINDING_LABEL);
589 info[MACHINE_BINDING_LABEL.len()..MACHINE_BINDING_LABEL.len() + 32]
590 .copy_from_slice(ephemeral_pk);
591 info[MACHINE_BINDING_LABEL.len() + 32..].copy_from_slice(machine_fp);
592
593 let hkdf = Hkdf::<Sha256>::new(Some(machine_fp.as_slice()), ss);
594 let mut kek = [0u8; 32];
595 hkdf.expand(&info, &mut kek).map_err(|_| {
596 Error::new(
597 Check::CryptKeyUnwrap,
598 "HKDF-SHA-256 expand failed for the machine-binding KEK",
599 )
600 })?;
601 Ok(kek)
602}
603
604fn aes_kw_wrap(kek: &[u8; 32], key: &[u8; 32]) -> Result<[u8; 40]> {
606 let mut wrapped = [0u8; 40];
607 KekAes256::from(*kek).wrap(key, &mut wrapped).map_err(|e| {
608 Error::new(
609 Check::CryptKeyUnwrap,
610 format!("AES-256-KW wrap failed: {e}"),
611 )
612 })?;
613 Ok(wrapped)
614}
615
616fn aes_kw_unwrap(kek: &[u8; 32], wrapped: &[u8; 40]) -> Result<[u8; 32]> {
618 let mut key = [0u8; 32];
619 KekAes256::from(*kek)
620 .unwrap(wrapped, &mut key)
621 .map_err(|e| {
622 Error::new(
623 Check::CryptKeyUnwrap,
624 format!("AES-256-KW unwrap failed: {e}"),
625 )
626 })?;
627 Ok(key)
628}
629
630fn aead_encrypt(
632 cipher: Cipher,
633 key: &[u8; 32],
634 nonce: &[u8; 12],
635 msg: &[u8],
636 aad: &[u8],
637) -> Result<Vec<u8>> {
638 let payload = Payload { msg, aad };
639 let nonce = GenericArray::from_slice(nonce);
640 let sealed = match cipher {
641 Cipher::Aes256Gcm => Aes256Gcm::new(GenericArray::from_slice(key)).encrypt(nonce, payload),
642 Cipher::ChaCha20Poly1305 => {
643 ChaCha20Poly1305::new(GenericArray::from_slice(key)).encrypt(nonce, payload)
644 }
645 };
646 sealed.map_err(|_| {
647 Error::new(
648 Check::CryptTagVerify,
649 format!("{cipher:?} encryption failed"),
650 )
651 })
652}
653
654fn aead_decrypt(
656 cipher: Cipher,
657 key: &[u8; 32],
658 nonce: &[u8],
659 msg: &[u8],
660 aad: &[u8],
661) -> Result<Vec<u8>> {
662 let payload = Payload { msg, aad };
663 let nonce = GenericArray::from_slice(nonce);
664 let opened = match cipher {
665 Cipher::Aes256Gcm => Aes256Gcm::new(GenericArray::from_slice(key)).decrypt(nonce, payload),
666 Cipher::ChaCha20Poly1305 => {
667 ChaCha20Poly1305::new(GenericArray::from_slice(key)).decrypt(nonce, payload)
668 }
669 };
670 opened.map_err(|_| {
671 Error::new(
672 Check::CryptTagVerify,
673 format!("{cipher:?} AEAD tag did not verify"),
674 )
675 })
676}
677
678#[cfg(test)]
679mod tests {
680 use super::*;
681
682 const TINY: (u32, u32, u8) = (1, 8, 1);
684 const PASSWORD: &str = "lumen-crypto-test";
685
686 fn session_key(byte: u8) -> SessionKey {
687 SessionKey::from_bytes([byte; 32])
688 }
689
690 fn keypair(seed: u8) -> ([u8; 32], [u8; 32]) {
692 let secret = StaticSecret::from([seed; 32]);
693 let public = PublicKey::from(&secret);
694 ([seed; 32], *public.as_bytes())
695 }
696
697 fn machine_auth(recipients: Vec<RecipientEntry>) -> Auth {
698 Auth {
699 cipher: Cipher::Aes256Gcm,
700 auth_version: AUTH_VERSION,
701 mode: MODE_MACHINE,
702 password: None,
703 recipients,
704 }
705 }
706
707 fn raw_auth(
709 cipher_id: [u8; 4],
710 version: u32,
711 mode: u32,
712 password: &[u8],
713 machine: &[u8],
714 ) -> Vec<u8> {
715 let mut w = Writer::new();
716 w.bytes(&cipher_id);
717 w.u32(version);
718 w.u32(mode);
719 w.u32(password.len() as u32);
720 w.u32(machine.len() as u32);
721 w.bytes(password);
722 w.bytes(machine);
723 w.into_vec()
724 }
725
726 fn low_order_entry(fingerprint: &[u8; 32], wrapped_session: &SessionKey) -> RecipientEntry {
730 let zero = [0u8; 32];
731 let kek = machine_kek(&zero, &zero, fingerprint).unwrap();
732 RecipientEntry {
733 machine_fp: *fingerprint,
734 ephemeral_pk: zero,
735 wrapped_key: aes_kw_wrap(&kek, wrapped_session.as_bytes()).unwrap(),
736 }
737 }
738
739 #[test]
740 fn generated_session_keys_are_random() {
741 let a = SessionKey::generate();
742 let b = SessionKey::generate();
743 assert_eq!(a.as_bytes().len(), 32);
744 assert_ne!(a, b);
745 assert_ne!(a.as_bytes(), &[0u8; 32]);
746 }
747
748 #[test]
749 fn associated_data_binds_chunk_type_and_unit_index() {
750 let aad = associated_data(ChunkType::LAYR, 0x0102_0304);
751 assert_eq!(&aad[..4], b"LAYR");
752 assert_eq!(aad[4], 0);
753 assert_eq!(&aad[5..], &[0x04, 0x03, 0x02, 0x01]);
754 }
755
756 #[test]
757 fn seal_open_round_trip_both_ciphers() {
758 let key = session_key(0x11);
759 let plaintext = b"LUMEN sealed unit payload";
760 for cipher in [Cipher::Aes256Gcm, Cipher::ChaCha20Poly1305] {
761 let sealed = seal(cipher, &key, ChunkType::META, 0, plaintext).unwrap();
762 assert_eq!(sealed.len(), plaintext.len() + UNIT_OVERHEAD);
763 assert_eq!(
764 open(cipher, &key, ChunkType::META, 0, &sealed).unwrap(),
765 plaintext.to_vec()
766 );
767
768 let empty = seal(cipher, &key, ChunkType::META, 0, b"").unwrap();
770 assert_eq!(empty.len(), UNIT_OVERHEAD);
771 assert!(open(cipher, &key, ChunkType::META, 0, &empty)
772 .unwrap()
773 .is_empty());
774
775 let again = seal(cipher, &key, ChunkType::META, 0, plaintext).unwrap();
777 assert_ne!(&sealed[..12], &again[..12]);
778 }
779 }
780
781 #[test]
782 fn open_rejects_tampered_or_misbound_units() {
783 let key = session_key(0x22);
784 let other_key = session_key(0x23);
785 for cipher in [Cipher::Aes256Gcm, Cipher::ChaCha20Poly1305] {
786 let sealed = seal(cipher, &key, ChunkType::LAYR, 3, b"block payload").unwrap();
787
788 let mut flipped_ciphertext = sealed.clone();
789 flipped_ciphertext[12] ^= 0x01;
790 assert_eq!(
791 open(cipher, &key, ChunkType::LAYR, 3, &flipped_ciphertext)
792 .unwrap_err()
793 .check(),
794 Check::CryptTagVerify
795 );
796
797 let mut flipped_tag = sealed.clone();
798 let last = flipped_tag.len() - 1;
799 flipped_tag[last] ^= 0x80;
800 assert_eq!(
801 open(cipher, &key, ChunkType::LAYR, 3, &flipped_tag)
802 .unwrap_err()
803 .check(),
804 Check::CryptTagVerify
805 );
806
807 assert_eq!(
809 open(
810 cipher,
811 &key,
812 ChunkType::LAYR,
813 3,
814 &sealed[..UNIT_OVERHEAD - 1]
815 )
816 .unwrap_err()
817 .check(),
818 Check::CryptTagVerify
819 );
820 assert_eq!(
821 open(cipher, &key, ChunkType::LAYR, 3, &sealed[..12])
822 .unwrap_err()
823 .check(),
824 Check::CryptTagVerify
825 );
826
827 assert_eq!(
829 open(cipher, &key, ChunkType::LAYR, 4, &sealed)
830 .unwrap_err()
831 .check(),
832 Check::CryptTagVerify
833 );
834 assert_eq!(
835 open(cipher, &key, ChunkType::META, 3, &sealed)
836 .unwrap_err()
837 .check(),
838 Check::CryptTagVerify
839 );
840
841 assert_eq!(
843 open(cipher, &other_key, ChunkType::LAYR, 3, &sealed)
844 .unwrap_err()
845 .check(),
846 Check::CryptTagVerify
847 );
848 }
849 }
850
851 #[test]
852 fn password_round_trip_recovers_the_session_key() {
853 let session = session_key(0x2a);
854 let (iterations, memory_kib, parallelism) = TINY;
855 let section =
856 wrap_password(&session, PASSWORD, iterations, memory_kib, parallelism).unwrap();
857 assert_eq!(section.iterations, 1);
858 assert_eq!(section.memory_kib, 8);
859 assert_eq!(section.parallelism, 1);
860 assert_ne!(section.salt, [0u8; 16]);
861 assert_eq!(check_argon2_budget(§ion), Ok(()));
862
863 let auth = Auth {
864 cipher: Cipher::Aes256Gcm,
865 auth_version: AUTH_VERSION,
866 mode: MODE_PASSWORD,
867 password: Some(section),
868 recipients: Vec::new(),
869 };
870 assert_eq!(
871 unwrap_password(&auth, PASSWORD).unwrap().as_bytes(),
872 session.as_bytes()
873 );
874 assert_eq!(
875 SessionKey::from_password(&auth, PASSWORD)
876 .unwrap()
877 .as_bytes(),
878 session.as_bytes()
879 );
880
881 let again = wrap_password(&session, PASSWORD, iterations, memory_kib, parallelism).unwrap();
883 assert_ne!(again.salt, section.salt);
884
885 assert_eq!(
887 unwrap_password(&auth, "lumen-crypto-tes")
888 .unwrap_err()
889 .check(),
890 Check::CryptKeyUnwrap
891 );
892
893 assert_eq!(
895 unwrap_password(&machine_auth(Vec::new()), PASSWORD)
896 .unwrap_err()
897 .check(),
898 Check::CryptNoKey
899 );
900 }
901
902 #[test]
903 fn argon2_budget_rejects_cost_above_the_ceiling() {
904 let base = PasswordSection {
905 salt: [0x5a; 16],
906 iterations: 1,
907 memory_kib: 8,
908 parallelism: 1,
909 wrapped_key: [0; 40],
910 };
911 assert_eq!(check_argon2_budget(&base), Ok(()));
912
913 let at_ceiling = PasswordSection {
914 iterations: ARGON2_MAX_ITERATIONS,
915 memory_kib: ARGON2_MAX_MEMORY_KIB,
916 parallelism: ARGON2_MAX_PARALLELISM as u8,
917 ..base
918 };
919 assert_eq!(check_argon2_budget(&at_ceiling), Ok(()));
920
921 let too_many_iterations = PasswordSection {
922 iterations: 99,
923 ..base
924 };
925 assert_eq!(
926 check_argon2_budget(&too_many_iterations)
927 .unwrap_err()
928 .check(),
929 Check::CryptArgon2Budget
930 );
931
932 let too_much_memory = PasswordSection {
933 memory_kib: ARGON2_MAX_MEMORY_KIB + 1,
934 ..base
935 };
936 assert_eq!(
937 check_argon2_budget(&too_much_memory).unwrap_err().check(),
938 Check::CryptArgon2Budget
939 );
940
941 let too_many_lanes = PasswordSection {
942 parallelism: (ARGON2_MAX_PARALLELISM + 1) as u8,
943 ..base
944 };
945 assert_eq!(
946 check_argon2_budget(&too_many_lanes).unwrap_err().check(),
947 Check::CryptArgon2Budget
948 );
949 }
950
951 #[test]
952 fn auth_round_trips_both_modes() {
953 let session = session_key(0x5c);
954 let (recipient_private, recipient_public) = keypair(0x21);
955 let password = wrap_password(&session, PASSWORD, 1, 8, 1).unwrap();
956 let entry = wrap_machine(&session, &recipient_public).unwrap();
957 let auth = Auth {
958 cipher: Cipher::ChaCha20Poly1305,
959 auth_version: AUTH_VERSION,
960 mode: MODE_PASSWORD | MODE_MACHINE,
961 password: Some(password),
962 recipients: vec![entry],
963 };
964
965 let encoded = encode_auth(&auth);
966 assert_eq!(&encoded[..4], b"C20P");
967 assert_eq!(
968 u32::from_le_bytes(encoded[4..8].try_into().unwrap()),
969 AUTH_VERSION
970 );
971 assert_eq!(
972 u32::from_le_bytes(encoded[8..12].try_into().unwrap()),
973 MODE_PASSWORD | MODE_MACHINE
974 );
975 assert_eq!(
976 u32::from_le_bytes(encoded[12..16].try_into().unwrap()),
977 PASSWORD_SECTION_LEN as u32
978 );
979 assert_eq!(
980 u32::from_le_bytes(encoded[16..20].try_into().unwrap()),
981 RECIPIENT_ENTRY_LEN as u32
982 );
983 assert_eq!(
984 encoded.len(),
985 AUTH_HEADER_LEN + PASSWORD_SECTION_LEN + RECIPIENT_ENTRY_LEN
986 );
987 assert_eq!(
988 &encoded[AUTH_HEADER_LEN..AUTH_HEADER_LEN + 16],
989 &password.salt
990 );
991 assert_eq!(
992 &encoded[AUTH_HEADER_LEN + 25..AUTH_HEADER_LEN + 65],
993 &password.wrapped_key
994 );
995 assert_eq!(
996 encoded[AUTH_HEADER_LEN + PASSWORD_SECTION_LEN + 64..],
997 entry.wrapped_key
998 );
999
1000 let parsed = parse_auth(&encoded).unwrap();
1001 assert_eq!(parsed, auth);
1002 assert_eq!(parsed.password.unwrap().wrapped_key, password.wrapped_key);
1003 assert_eq!(parsed.recipients, vec![entry]);
1004 assert_eq!(
1005 unwrap_password(&parsed, PASSWORD).unwrap().as_bytes(),
1006 session.as_bytes()
1007 );
1008 assert_eq!(
1009 unwrap_machine(&parsed, &recipient_private)
1010 .unwrap()
1011 .as_bytes(),
1012 session.as_bytes()
1013 );
1014 }
1015
1016 #[test]
1017 fn auth_round_trips_single_mode_layouts() {
1018 let session = session_key(0x60);
1019
1020 let password = wrap_password(&session, PASSWORD, 1, 8, 1).unwrap();
1021 let password_only = Auth {
1022 cipher: Cipher::Aes256Gcm,
1023 auth_version: AUTH_VERSION,
1024 mode: MODE_PASSWORD,
1025 password: Some(password),
1026 recipients: Vec::new(),
1027 };
1028 let encoded = encode_auth(&password_only);
1029 assert_eq!(encoded.len(), AUTH_HEADER_LEN + PASSWORD_SECTION_LEN);
1030 assert_eq!(parse_auth(&encoded).unwrap(), password_only);
1031
1032 let first = wrap_machine(&session, &keypair(0x31).1).unwrap();
1033 let second = wrap_machine(&session, &keypair(0x32).1).unwrap();
1034 let machine_only = machine_auth(vec![first, second]);
1035 let encoded = encode_auth(&machine_only);
1036 assert_eq!(encoded.len(), AUTH_HEADER_LEN + 2 * RECIPIENT_ENTRY_LEN);
1037 assert_eq!(parse_auth(&encoded).unwrap(), machine_only);
1038 }
1039
1040 #[test]
1041 fn parse_auth_ignores_surplus_password_section_bytes() {
1042 let session = session_key(0x77);
1043 let section = wrap_password(&session, PASSWORD, 1, 8, 1).unwrap();
1044 let auth = Auth {
1045 cipher: Cipher::Aes256Gcm,
1046 auth_version: AUTH_VERSION,
1047 mode: MODE_PASSWORD,
1048 password: Some(section),
1049 recipients: Vec::new(),
1050 };
1051
1052 let encoded = encode_auth(&auth);
1053 let mut padded = encoded[AUTH_HEADER_LEN..].to_vec();
1054 padded.extend_from_slice(&[0u8; 5]);
1055
1056 let payload = raw_auth(CIPHER_A256, AUTH_VERSION, MODE_PASSWORD, &padded, &[]);
1057 assert_eq!(
1058 u32::from_le_bytes(payload[12..16].try_into().unwrap()),
1059 (PASSWORD_SECTION_LEN + 5) as u32
1060 );
1061
1062 let parsed = parse_auth(&payload).unwrap();
1063 assert_eq!(parsed.password, Some(section));
1064 assert_eq!(
1065 unwrap_password(&parsed, PASSWORD).unwrap().as_bytes(),
1066 session.as_bytes()
1067 );
1068 }
1069
1070 #[test]
1071 fn parse_auth_rejects_malformed_frames() {
1072 let password = [0u8; PASSWORD_SECTION_LEN];
1073
1074 assert_eq!(
1076 parse_auth(&[0u8; AUTH_HEADER_LEN - 1]).unwrap_err().check(),
1077 Check::AuthFrame
1078 );
1079 assert_eq!(parse_auth(&[]).unwrap_err().check(), Check::AuthFrame);
1080
1081 let mut overrun = raw_auth(CIPHER_A256, AUTH_VERSION, MODE_MACHINE, &[], &[]);
1083 overrun[16..20].copy_from_slice(&(RECIPIENT_ENTRY_LEN as u32).to_le_bytes());
1084 assert_eq!(parse_auth(&overrun).unwrap_err().check(), Check::AuthFrame);
1085
1086 assert_eq!(
1088 parse_auth(&raw_auth(
1089 *b"XXXX",
1090 AUTH_VERSION,
1091 MODE_PASSWORD,
1092 &password,
1093 &[]
1094 ))
1095 .unwrap_err()
1096 .check(),
1097 Check::AuthCipherKnown
1098 );
1099 assert_eq!(
1100 parse_auth(&raw_auth(CIPHER_A256, 2, MODE_PASSWORD, &password, &[]))
1101 .unwrap_err()
1102 .check(),
1103 Check::AuthVersion
1104 );
1105
1106 assert_eq!(
1108 parse_auth(&raw_auth(CIPHER_A256, AUTH_VERSION, 0, &[], &[]))
1109 .unwrap_err()
1110 .check(),
1111 Check::CryptModeEmpty
1112 );
1113
1114 assert_eq!(
1116 parse_auth(&raw_auth(
1117 CIPHER_A256,
1118 AUTH_VERSION,
1119 MODE_PASSWORD,
1120 &password[..PASSWORD_SECTION_LEN - 1],
1121 &[]
1122 ))
1123 .unwrap_err()
1124 .check(),
1125 Check::CryptPasswordSectionLen
1126 );
1127
1128 assert_eq!(
1130 parse_auth(&raw_auth(CIPHER_C20P, AUTH_VERSION, MODE_MACHINE, &[], &[]))
1131 .unwrap_err()
1132 .check(),
1133 Check::CryptMachineSectionLen
1134 );
1135 assert_eq!(
1136 parse_auth(&raw_auth(
1137 CIPHER_C20P,
1138 AUTH_VERSION,
1139 MODE_MACHINE,
1140 &[],
1141 &[0u8; RECIPIENT_ENTRY_LEN + 1]
1142 ))
1143 .unwrap_err()
1144 .check(),
1145 Check::CryptMachineSectionLen
1146 );
1147 }
1148
1149 #[test]
1150 fn machine_round_trip_recovers_the_session_key() {
1151 let session = session_key(0x33);
1152 let (private, public) = keypair(0x07);
1153 let entry = wrap_machine(&session, &public).unwrap();
1154 assert_eq!(entry.machine_fp, machine_fingerprint(&public));
1155 assert_ne!(entry.ephemeral_pk, [0u8; 32]);
1156
1157 assert_eq!(
1158 unwrap_machine(&machine_auth(vec![entry]), &private)
1159 .unwrap()
1160 .as_bytes(),
1161 session.as_bytes()
1162 );
1163
1164 let other = wrap_machine(&session, &public).unwrap();
1166 assert_eq!(other.machine_fp, entry.machine_fp);
1167 assert_ne!(other.ephemeral_pk, entry.ephemeral_pk);
1168 assert_ne!(other.wrapped_key, entry.wrapped_key);
1169 }
1170
1171 #[test]
1172 fn machine_unwrap_skips_recipients_for_other_machines() {
1173 let session = session_key(0x44);
1174 let (stranger_private, stranger_public) = keypair(0x08);
1175 let (our_private, our_public) = keypair(0x09);
1176 let auth = machine_auth(vec![
1177 wrap_machine(&session, &stranger_public).unwrap(),
1178 wrap_machine(&session, &our_public).unwrap(),
1179 ]);
1180
1181 assert_eq!(
1183 unwrap_machine(&auth, &our_private).unwrap().as_bytes(),
1184 session.as_bytes()
1185 );
1186 assert_eq!(
1188 unwrap_machine(&auth, &stranger_private).unwrap().as_bytes(),
1189 session.as_bytes()
1190 );
1191
1192 let (absent_private, _) = keypair(0x0a);
1194 assert_eq!(
1195 unwrap_machine(&auth, &absent_private).unwrap_err().check(),
1196 Check::CryptNoKey
1197 );
1198 assert_eq!(
1199 unwrap_machine(&machine_auth(Vec::new()), &our_private)
1200 .unwrap_err()
1201 .check(),
1202 Check::CryptNoKey
1203 );
1204 }
1205
1206 #[test]
1207 fn machine_unwrap_skips_low_order_entry_and_keeps_searching() {
1208 let session = session_key(0x55);
1209 let decoy_session = session_key(0x56);
1210 let (our_private, our_public) = keypair(0x0b);
1211 let fingerprint = machine_fingerprint(&our_public);
1212
1213 let decoy = low_order_entry(&fingerprint, &decoy_session);
1214 let zero = [0u8; 32];
1217 let decoy_kek = machine_kek(&zero, &zero, &fingerprint).unwrap();
1218 assert_eq!(
1219 aes_kw_unwrap(&decoy_kek, &decoy.wrapped_key).unwrap(),
1220 *decoy_session.as_bytes()
1221 );
1222
1223 let auth = machine_auth(vec![decoy, wrap_machine(&session, &our_public).unwrap()]);
1224 let recovered = unwrap_machine(&auth, &our_private).unwrap();
1225 assert_eq!(recovered.as_bytes(), session.as_bytes());
1226 assert_ne!(recovered.as_bytes(), decoy_session.as_bytes());
1227 }
1228
1229 #[test]
1230 fn machine_unwrap_reports_low_order_when_every_match_is_rejected() {
1231 let decoy_session = session_key(0x56);
1232 let (our_private, our_public) = keypair(0x0b);
1233 let fingerprint = machine_fingerprint(&our_public);
1234
1235 let auth = machine_auth(vec![low_order_entry(&fingerprint, &decoy_session)]);
1236 assert_eq!(
1237 unwrap_machine(&auth, &our_private).unwrap_err().check(),
1238 Check::CryptLowOrderPoint
1239 );
1240 }
1241
1242 #[test]
1243 fn machine_unwrap_rejects_a_matching_entry_that_does_not_unwrap() {
1244 let session = session_key(0x66);
1245 let (our_private, our_public) = keypair(0x0c);
1246 let mut entry = wrap_machine(&session, &our_public).unwrap();
1247 entry.wrapped_key[0] ^= 0x01;
1248
1249 assert_eq!(
1250 unwrap_machine(&machine_auth(vec![entry]), &our_private)
1251 .unwrap_err()
1252 .check(),
1253 Check::CryptKeyUnwrap
1254 );
1255 }
1256}