1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
//! Cryptography layer for Mnemo (Phase 4 of the build plan).
//!
//! Mnemo uses a **two-tier key hierarchy**:
//!
//! ```text
//! passphrase ──Argon2id──▶ KEK (key-encryption-key)
//! │ AES-256-GCM
//! ▼
//! DEK (data-encryption-key, random 256-bit)
//! │ AES-256-GCM, per-page
//! ▼
//! encrypted pages on disk
//! ```
//!
//! The DEK encrypts every page. The KEK only ever encrypts ("wraps") the DEK.
//! This is what makes `rekey` cheap: changing the passphrase re-derives the KEK
//! and re-wraps the DEK — the pages themselves never need re-encryption.
//!
//! Every page gets a 96-bit nonce minted at write time from a per-session
//! random prefix plus a per-session monotonic counter (SEC-1). The random
//! prefix is drawn fresh when a `Mnemo` handle opens the file — it never
//! touches the disk — and the counter resets to zero in the same step.
//! Together they make an AES-GCM nonce reuse infeasible across the sessions
//! that could ever share one DEK, including the "two forks of the same file
//! after a backup" case that an on-disk `write_counter`-based scheme leaks.
use ;
use ;
use ;
use Zeroizing;
use crate;
/// Length of a symmetric key in bytes (256-bit).
pub const KEY_LEN: usize = 32;
/// Length of an AES-GCM nonce in bytes (96-bit).
pub const NONCE_LEN: usize = 12;
/// Length of an AES-GCM authentication tag in bytes.
pub const TAG_LEN: usize = 16;
/// Length of the Argon2 salt in bytes.
pub const SALT_LEN: usize = 16;
/// Argon2id cost parameters. Stored (unencrypted) in the file header so the
/// KEK can be re-derived on open.
/// Derive the key-encryption-key (KEK) from a passphrase using Argon2id.
/// AES-256-GCM encrypt with **Associated Authenticated Data**. The AAD is not
/// part of the ciphertext but is bound into the authentication tag; decryption
/// fails unless the caller supplies the same AAD. Output is
/// `ciphertext || 16-byte tag`.
///
/// Pass `&[]` to encrypt without AAD — this matches the format before v6, used
/// for wrapping the DEK and for v4/v5 page encryption. From v6 onwards, page
/// encryption passes `page_no.to_le_bytes()` as AAD so an attacker can't
/// transplant a valid encrypted page to a different page slot — the
/// authentication tag binds the page image to its home page number.
/// AES-256-GCM decrypt with AAD. Input must be `ciphertext || 16-byte tag`.
/// Returns an error if authentication fails — including when the supplied
/// AAD doesn't match what was used during encryption.
/// Generate a fresh random 256-bit data-encryption-key.
/// Generate a random Argon2 salt.
/// Generate a random nonce (used for wrapping the DEK).
/// Generate a random `u64`, used by the pager to pick a per-session
/// nonce prefix. See [`session_page_nonce`] for the full construction.
/// Build a per-session page nonce (PR SEC-1, v0.5.2).
///
/// Structure, following NIST SP 800-38D §8.2.1 (fixed field + invocation
/// field) for AES-GCM:
///
/// - **Bytes 0..8** — `session_random`: a 64-bit random value drawn
/// fresh on every writable [`crate::Mnemo::open`] / [`crate::Mnemo::create`]
/// (and regenerated on counter wrap). This is the "fixed field".
/// - **Bytes 8..12** — `session_counter`: a 32-bit per-session monotonic
/// counter that advances once per encrypted page write. This is the
/// "invocation field".
///
/// `page_no` is **not** encoded in the nonce. Pages were bound to their
/// home slot through AAD since v6 (`page_no.to_le_bytes()`), so an
/// attacker who swapped a valid page to a different slot already failed
/// authentication — the AAD binding stays in place and does the job the
/// `page_no` component of the pre-SEC-1 nonce was helping with.
///
/// # Why not deterministic any more
///
/// Before v0.5.2 the nonce was `page_no as u32 || write_counter as u64`,
/// which was unique **within a file** but not **across copies of a file**.
/// If you copied a `.mnemo` file to another host and both the original and
/// the copy got written to (two laptops syncing a Dropbox file; a
/// restored older backup resuming operation), each copy's writer produced
/// pages whose nonces collided with the other's at the same `(page_no,
/// write_counter)` coordinates, under the same DEK (`random_dek()` only
/// at create; `rekey` keeps it). That breaks AES-GCM's confidentiality
/// and tamper-evidence guarantee: whoever holds both ciphertexts can
/// recover the XOR of the plaintexts and forge pages with a valid tag.
///
/// A fresh random 64-bit `session_random` per writable open closes that:
/// cross-copy collision now requires two sessions to independently draw
/// the same 64-bit value (birthday bound ~2^32 sessions, i.e.
/// astronomically unlikely for a human-scale user). The 32-bit
/// `session_counter` guarantees uniqueness *within* a session; the pager
/// regenerates `session_random` and resets the counter if it would wrap.
///
/// # Decrypt is unaffected
///
/// Each page already stores its own nonce in bytes `[0..12]` of the
/// on-disk image (see `mnemo/src/pager.rs::read_page`). Decrypt reads the
/// stored nonce; it never recomputes one. **No format bump — a v0.5.1
/// file opens fine on v0.5.2 and vice versa.**
/// Wrap (encrypt) the DEK with the KEK. The wrap uses **empty AAD** in every
/// version, so v5 wrapped-DEK bytes on disk round-trip unchanged through a v6
/// build — only page encryption changed in v6.
/// Unwrap (decrypt) the DEK with the KEK. A wrong KEK (wrong passphrase)
/// fails authentication and is reported as [`MnemoError::WrongPassphrase`].