|
| 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. |
| 110 | +pub mod cached; |
| 111 | +/// Low-level age-format encryption with a caller-supplied FileKey. |
| 112 | +pub mod encrypt; |
| 113 | +/// Full-KDF identity for passphrase-based decryption. |
| 114 | +pub mod identity; |
| 115 | +/// Validated Argon2id parameters. |
| 116 | +pub mod params; |
| 117 | +/// Full-KDF recipient for passphrase-based encryption. |
| 118 | +pub mod recipient; |
| 119 | + |
| 120 | +pub use cached::{CachedIdentity, CachedMaterial, CachedRecipient}; |
| 121 | +pub use encrypt::{encrypt_with_file_key, EncryptWithFileKeyError}; |
| 122 | +pub use identity::Argon2idIdentity; |
| 123 | +pub use params::{Argon2Params, InvalidParams}; |
| 124 | +pub use recipient::Argon2idRecipient; |
0 commit comments