Skip to main content

age_plugin_argon2/
lib.rs

1#![warn(missing_docs)]
2
3//! Argon2id recipient/identity plugin for the age encryption format.
4//!
5//! This crate provides password-based encryption for [age] files using Argon2id
6//! key derivation instead of scrypt. It also provides cached variants that skip
7//! the KDF entirely for session-based workflows.
8//!
9//! [age]: https://age-encryption.org
10//!
11//! # Quick start
12//!
13//! Full KDF encrypt → decrypt roundtrip:
14//!
15//! ```rust
16//! use age_plugin_argon2::{Argon2idRecipient, Argon2idIdentity, Argon2Params};
17//! use age::{Recipient, Identity};
18//! use age_core::format::FileKey;
19//! use secrecy::ExposeSecret;
20//!
21//! let passphrase = b"hunter2";
22//! let params = Argon2Params::new(256, 1, 1).unwrap(); // use stronger params in production
23//!
24//! // Encrypt
25//! let recipient = Argon2idRecipient::new(passphrase, params);
26//! let file_key = FileKey::new(Box::new([0u8; 16]));
27//! let (stanzas, _labels) = recipient.wrap_file_key(&file_key).unwrap();
28//!
29//! // Decrypt
30//! let identity = Argon2idIdentity::new(passphrase);
31//! let recovered = identity.unwrap_stanza(&stanzas[0]).unwrap().unwrap();
32//! assert_eq!(recovered.expose_secret(), file_key.expose_secret());
33//! ```
34//!
35//! # Cached / session mode
36//!
37//! After an initial KDF decryption, captured material can be reused to avoid
38//! running Argon2id on every subsequent encrypt/decrypt:
39//!
40//! ```rust
41//! use age_plugin_argon2::{Argon2idRecipient, Argon2idIdentity, CachedRecipient, CachedIdentity, Argon2Params};
42//! use age::{Recipient, Identity};
43//! use age_core::format::FileKey;
44//! use secrecy::ExposeSecret;
45//!
46//! let passphrase = b"hunter2";
47//! let params = Argon2Params::new(256, 1, 1).unwrap();
48//!
49//! // Initial full-KDF encrypt + decrypt to capture session material
50//! let (stanzas, _) = Argon2idRecipient::new(passphrase, params)
51//!     .wrap_file_key(&FileKey::new(Box::new([0u8; 16])))
52//!     .unwrap();
53//! let identity = Argon2idIdentity::new(passphrase);
54//! identity.unwrap_stanza(&stanzas[0]).unwrap().unwrap();
55//! let material = identity.captured_material().unwrap();
56//!
57//! // Session re-encrypt without running Argon2id
58//! let session_key = FileKey::new(Box::new(material.file_key));
59//! let (session_stanzas, _) = CachedRecipient::new(&material)
60//!     .wrap_file_key(&session_key)
61//!     .unwrap();
62//!
63//! // Session decrypt — also skips KDF
64//! let recovered = CachedIdentity::new(&material)
65//!     .unwrap_stanza(&session_stanzas[0])
66//!     .unwrap()
67//!     .unwrap();
68//! assert_eq!(recovered.expose_secret(), &[0u8; 16]);
69//! ```
70//!
71//! # Security model
72//!
73//! Two operational modes with different security/performance trade-offs:
74//!
75//! ## Full KDF (`Argon2idRecipient` / `Argon2idIdentity`)
76//!
77//! Used at session boundaries (init, unlock). Every encrypt/decrypt runs the
78//! full Argon2id KDF to derive a wrapping key from the passphrase + random salt.
79//! The wrapping key protects the age FileKey via ChaCha20-Poly1305 AEAD.
80//!
81//! - **Encrypt**: random salt → Argon2id → wrapping key → AEAD-wrap FileKey
82//! - **Decrypt**: parse salt from stanza → Argon2id → wrapping key → AEAD-unwrap FileKey
83//! - **Key capture**: on successful decrypt, `Argon2idIdentity` captures the
84//!   FileKey + wrapping key + salt as [`CachedMaterial`] for session caching
85//!
86//! ## Cached / zero-KDF (`CachedRecipient` / `CachedIdentity`)
87//!
88//! Used during an active session after the initial unlock. The passphrase is
89//! never stored — only opaque key material (64 bytes) lives in the OS keychain.
90//!
91//! - **`CachedRecipient`** (writes): reuses the captured wrapping key + salt to
92//!   AEAD-wrap the FileKey without running Argon2id. Produces stanzas
93//!   indistinguishable from full-KDF output.
94//! - **`CachedIdentity`** (reads): returns the cached FileKey directly.
95//!   Stanza body verification is intentionally skipped because the age STREAM
96//!   layer provides per-chunk Poly1305 authentication — a wrong FileKey will
97//!   fail at payload decryption, not silently produce garbage.
98//!
99//! ## Stanza format
100//!
101//! ```text
102//! -> thesis.co/argon2 <base64-salt> <m_cost> <t_cost> <p_cost>
103//! <AEAD-wrapped FileKey>
104//! ```
105//!
106//! The namespaced tag (`thesis.co/argon2`) avoids collisions with any future
107//! upstream age scrypt/argon2 recipient type.
108
109/// Cached / zero-KDF recipient and identity for session use.
110pub mod cached;
111/// Low-level age-format encryption with a caller-supplied FileKey.
112pub mod encrypt;
113/// Full-KDF identity for passphrase-based decryption.
114pub mod identity;
115/// Validated Argon2id parameters.
116pub mod params;
117/// Full-KDF recipient for passphrase-based encryption.
118pub mod recipient;
119
120pub use cached::{CachedIdentity, CachedMaterial, CachedRecipient};
121pub use encrypt::{encrypt_with_file_key, EncryptWithFileKeyError};
122pub use identity::Argon2idIdentity;
123pub use params::{Argon2Params, InvalidParams};
124pub use recipient::Argon2idRecipient;