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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
//! [`SealedEnvelope`] — the on-the-wire payload produced by [`seal`] and
//! consumed by [`open`].
//!
//! Encryption is X25519 ephemeral key agreement plus AES-256-GCM with a
//! HKDF-SHA256-derived key and nonce. Each [`seal`] generates a fresh
//! ephemeral X25519 keypair; the ephemeral public half travels in the
//! envelope, the secret half drops at end of scope (forward secrecy per
//! envelope). The recipient's long-term X25519 key is required to recompute
//! the shared secret and decrypt.
//!
//! HKDF expansion produces 44 bytes — 32 for the AES-256 key and 12 for the
//! GCM nonce — so the nonce is bound to the shared secret and never reused
//! by accident across distinct seals (fresh ephemeral key ⇒ fresh shared
//! secret ⇒ fresh nonce). The HKDF `info` input binds both public keys
//! (`SEAL_INFO || ephemeral_public_key || recipient_public_key`, the `NaCl`
//! `crypto_box_seal` pattern) so the derived key is committed to the exact
//! key pair that produced the shared secret — defense in depth against any
//! key-substitution confusion.
//!
//! **Contributory-behaviour check.** X25519 accepts low-order points and
//! silently produces an all-zero shared secret for them. Both [`seal`] and
//! [`open`] reject non-contributory Diffie-Hellman results: sealing to a
//! low-order recipient key fails with [`TrustError::Seal`], and opening an
//! envelope whose ephemeral point is low-order fails with
//! [`TrustError::UnsealFailed`]. An attacker can therefore never force a
//! predictable shared secret.
//!
//! **Single failure arbiter.** [`open`] performs no independent plausibility
//! checks on the envelope fields — AES-GCM tag verification is the sole
//! arbiter of decryption failure. A wrong recipient key yields a wrong
//! derived AES key; a tampered nonce or ephemeral point yields a key/nonce
//! pair that never encrypted the ciphertext; every case surfaces as the same
//! constant-shape GCM tag failure mapped to [`TrustError::UnsealFailed`].
//! This preserves the non-oracle property: the caller (and any observer of
//! timing) cannot learn *why* an unseal failed.
//!
//! The unsealed payload is returned as `Vec<u8>`. The trust crate does not
//! interpret it — consumers decide what the bytes mean (sealed API token,
//! credential bundle, etc.).
use ;
use ;
use Hkdf;
use RngCore;
use ;
use Sha256;
use ;
use Zeroizing;
use crate;
/// Domain-separation tag for HKDF expansion. Bound to this crate's
/// sealed-envelope format so the same shared secret used in any other
/// protocol context (e.g. session-key derivation in a consumer) cannot
/// collide. The full HKDF `info` is this tag concatenated with the ephemeral
/// and recipient public keys — see [`derive_key_and_nonce`].
const SEAL_INFO: & = b"lys-sealed-envelope/v1";
/// HKDF output length: 32-byte AES-256 key + 12-byte AES-GCM nonce.
const HKDF_OUTPUT_LEN: usize = 44;
/// AES-256 key length in bytes.
const AES_KEY_LEN: usize = 32;
/// AES-GCM nonce length in bytes.
const NONCE_LEN: usize = 12;
// The split in `derive_key_and_nonce` consumes the OKM exactly: the key takes
// `okm[..AES_KEY_LEN]` and the nonce takes all of `okm[AES_KEY_LEN..]`. If these
// three constants ever stopped agreeing, that second slice would be the wrong
// length for the nonce array and `copy_from_slice` would panic — in library
// code, which this crate forbids. The relationship was previously stated only in
// the doc comment on `HKDF_OUTPUT_LEN`, i.e. held by three constants happening to
// agree and by prose that does not run. This makes a mismatch fail to compile
// rather than fail at runtime.
const _: = assert!;
/// Sealed payload bound to a single recipient X25519 public key.
///
/// Constructed by [`seal`] and consumed by [`open`]. The envelope is the
/// wire format — `serde` is derived so consumers can carry it through any
/// transport or storage layer.
/// Encrypts `payload` for the holder of the X25519 secret matching
/// `recipient_public_key`, returning a [`SealedEnvelope`].
///
/// A fresh X25519 ephemeral keypair is generated per call. The ephemeral
/// secret is combined with the recipient's public key via Diffie-Hellman;
/// the shared secret is run through HKDF-SHA256 with an `info` input of
/// `SEAL_INFO || ephemeral_public_key || recipient_public_key` to produce a
/// 32-byte AES-256 key and a 12-byte AES-GCM nonce. The nonce is derived
/// from the same expansion as the key, so it is unique whenever the shared
/// secret is unique (which is always, since the ephemeral key is fresh).
/// Binding both public keys into the derivation commits the key material to
/// the exact key pair involved (`NaCl` `crypto_box_seal` pattern).
///
/// The Diffie-Hellman result is checked for contributory behaviour: a
/// recipient public key that is a low-order point would force an all-zero
/// shared secret regardless of the ephemeral secret, so it is rejected.
///
/// # Errors
///
/// Returns [`TrustError::Seal`] if `recipient_public_key` is a low-order
/// point (non-contributory Diffie-Hellman), if HKDF expansion fails (only
/// possible for output lengths above 255 × 32 bytes — never for the 44-byte
/// output produced here), or if AES-GCM encryption fails (only possible if
/// the payload length exceeds `2^39 - 256` bytes, the AES-GCM hard limit).
/// Decrypts `envelope` using `recipient_private_key`.
///
/// Recomputes the X25519 shared secret from `envelope.ephemeral_public_key`
/// and `recipient_private_key` (rejecting low-order ephemeral points via the
/// contributory-behaviour check), re-derives the AES-256 key via the same
/// HKDF expansion used by [`seal`] — binding the envelope's ephemeral public
/// key and the recipient public key derived from `recipient_private_key`
/// into the `info` input — and decrypts the ciphertext using
/// `envelope.nonce` as the operative nonce.
///
/// AES-GCM tag verification is the single failure arbiter: a wrong recipient
/// key derives a wrong AES key, a tampered nonce or ephemeral point breaks
/// the key/nonce binding, and each case fails the tag check identically. No
/// independent pre-checks are performed, so failure timing does not reveal
/// *which* field was wrong. Every unseal failure — wrong key, tampered
/// ciphertext, tampered nonce, tampered or low-order ephemeral key, or the
/// practically unreachable HKDF expansion failure — collapses to
/// [`TrustError::UnsealFailed`]; the caller cannot distinguish them by error
/// variant, which is the desired non-oracle behaviour.
///
/// # Errors
///
/// Returns [`TrustError::UnsealFailed`] if the ephemeral public key is a
/// low-order point, if AES-GCM authentication or decryption fails, or in
/// the practically unreachable case that HKDF expansion fails.
/// HKDF-SHA256 expansion of the X25519 shared secret into the AES key and
/// GCM nonce used by [`seal`] and [`open`].
///
/// The `info` input is `SEAL_INFO || ephemeral_public_key ||
/// recipient_public_key`, binding the derived key material to both halves of
/// the key agreement (the `NaCl` `crypto_box_seal` pattern). Both sides must
/// pass identical values: [`seal`] has both keys directly, [`open`] takes
/// the ephemeral key from the envelope and derives the recipient public key
/// from the recipient's private key.
///
/// The AES key is returned wrapped in [`Zeroizing`] so it is overwritten on
/// drop; the nonce is not a secret and ships in the envelope.