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
265
266
267
268
269
270
//! AES-256-GCM authenticated encryption.
//!
//! # What this does
//!
//! Encrypts / decrypts the secret-bytes payload under a 32-byte key derived
//! from the user's password (via [`crate::kdf`]) and a 12-byte random nonce.
//! The keystore header bytes are bound into the authentication tag as
//! associated data (AAD) so any edit to the header invalidates the MAC.
//!
//! # Why AES-256-GCM
//!
//! - **AES-256**: resists quantum square-root attacks better than AES-128
//! (Grover reduces 256→128 security bits, still comfortable).
//! - **GCM mode**: built-in authenticated encryption with a 128-bit MAC tag;
//! no separate HMAC needed; widely audited.
//! - **96-bit nonce**: the size GCM was designed around. We generate a fresh
//! random nonce per file (re-used on `change_password` / `rotate_kdf` —
//! actually re-generated fresh each time).
//!
//! # Nonce-reuse safety
//!
//! AES-GCM is catastrophically broken if a `(key, nonce)` pair is ever reused
//! to encrypt two distinct plaintexts (reveals the MAC key). This module
//! never re-uses a nonce because:
//!
//! 1. Every `create` / `change_password` / `rotate_kdf` generates a fresh
//! random nonce via `rand_core::OsRng` (or the caller's RNG).
//! 2. The nonce is stored in the file header and the key is password-derived;
//! both are regenerated together on each re-encryption.
//!
//! The `_with_rng` methods allow deterministic nonces in tests — those tests
//! run with fresh backends so no cross-run collision is possible.
//!
//! # AAD binding
//!
//! We feed the 53-byte keystore header as AAD. This means:
//!
//! - Flipping any header byte (magic, scheme id, KDF params, salt, nonce,
//! payload_len) invalidates the tag → `DecryptFailed`.
//! - An attacker cannot swap e.g. a `BlsSigning` header onto an `L1WalletBls`
//! ciphertext without knowing the key.
//!
//! # References
//!
//! - [RFC 5116 — AEAD APIs](https://datatracker.ietf.org/doc/html/rfc5116)
//! - [NIST SP 800-38D — GCM](https://csrc.nist.gov/publications/detail/sp/800-38d/final)
//! - [`aes-gcm` crate](https://docs.rs/aes-gcm) — RustCrypto implementation
use ;
use ;
use Zeroizing;
use crate;
/// Encrypt `plaintext` under the 32-byte `key` and 12-byte `nonce`, binding
/// `aad` into the authentication tag.
///
/// The output is `ciphertext || tag` (AES-GCM convention). The tag is a fixed
/// 16 bytes appended at the end; total output length is `plaintext.len() + 16`.
///
/// # Parameters
///
/// - `key`: 32-byte AES-256 key derived from the password via
/// [`crate::kdf::derive_key`].
/// - `nonce`: 12-byte random nonce; **must** be unique per-key.
/// - `plaintext`: the secret bytes to encrypt (typically a 32-byte seed).
/// - `aad`: additional authenticated data. This crate passes the keystore
/// header bytes so header edits invalidate the tag.
///
/// # Errors
///
/// Returns [`KeystoreError::DecryptFailed`] on any `aead::Error`. The `aes-gcm`
/// crate does not surface sub-errors, so we map them all to the single decrypt
/// variant.
pub
/// Decrypt a combined `ciphertext || tag` blob.
///
/// Returns the plaintext wrapped in [`Zeroizing`] so it wipes on drop. The
/// AAD must match what was passed at encrypt time — this is how the crate
/// binds the keystore header to the ciphertext.
///
/// # Parameters
///
/// - `key`: 32-byte AES-256 key re-derived from the password.
/// - `nonce`: 12-byte nonce read from the file header.
/// - `ciphertext_and_tag`: the raw payload bytes from the file
/// (`plaintext.len() + 16` bytes).
/// - `aad`: must be the exact same bytes passed as `aad` at encrypt time —
/// for this crate, the 53-byte header.
///
/// # Errors
///
/// Returns [`KeystoreError::DecryptFailed`] for any authentication failure:
/// wrong key (wrong password), wrong nonce, tampered ciphertext, tampered AAD
/// (header edit). We do NOT distinguish these at the error level — that would
/// leak a side channel about *why* the decrypt failed, which an attacker could
/// exploit to tell "wrong password" from "modified file".
pub
/// AES-GCM tag size in bytes.
///
/// Exposed at crate level so [`crate::keystore`] and [`crate::format`] can
/// reason about the total file-length arithmetic (`payload_len = secret.len() + TAG_SIZE`).
pub const TAG_SIZE: usize = 16;