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
//! `crypto_secretbox` equivalent (`docs/dstu-crypto-project.md` "Mapping onto the libsodium API",
//! `docs/TASKS.md` T-37, `docs/DECISIONS.md` D-51) - a single fixed `hazmat::kalyna_gcm::Kalyna256_256Gcm`
//! construction (D-47's tie-breaker rule: no algorithm knob when one safe default exists) with an
//! internally-generated nonce (never caller-supplied, extending the pattern `uacrypt kalyna-ccm
//! encrypt`'s CLI layer already used, D-40/T-82) and a combined `nonce || ciphertext || tag` wire
//! format, matching libsodium's own `crypto_secretbox_easy` ergonomics.
//!
//! # No message-length cap
//!
//! Migrated from Kalyna-CCM to Kalyna-GCM 2026-07-25 (roadmap Step 3 item 1, `docs/DECISIONS.md`
//! D-63) - the original Kalyna-CCM construction capped plaintext/AAD at 255 bytes each (D-41,
//! `ccm_padd`'s header encoding). GCM encodes no length into its construction at all, so that cap
//! and `SecretboxError::MessageTooLong` are gone entirely, not just raised. This does not make
//! disk-file encryption memory-bounded, though: an AEAD tag needs the full plaintext/ciphertext,
//! so a large message still means a correspondingly large in-memory buffer (see `uacrypt`'s own
//! `run_secretbox_command` doc comment for the concrete consequence at the CLI layer).
//! `crypto_secretstream` (`docs/TASKS.md` T-40) remains the separately-tracked follow-up for a
//! genuinely chunked/streaming construction; this module still does not attempt that.
//!
//! # No AAD (caller-facing) - but the nonce is bound into the tag internally
//!
//! libsodium's own `crypto_secretbox` has no associated-data parameter (that's `crypto_aead`'s
//! job) - `hazmat::kalyna_gcm` takes AAD, but exposing it here would quietly turn this into a
//! different primitive than its name promises. No caller-supplied AAD exists.
//!
//! Internally, though, `seal`/`open` pass the nonce itself as `kalyna_gcm`'s AAD (never empty).
//! This is not optional: unlike NIST AES-GCM, DSTU 7624's Kalyna-GCM tag is computed purely from
//! AAD and ciphertext (`E_K(accumulator XOR length_block)`, D-56 divergence 3) and never mixes in
//! the IV/nonce at all - the nonce only seeds the keystream. For a combined
//! `nonce || ciphertext || tag` blob, an unauthenticated nonce means an attacker can flip bits in
//! the nonce prefix of a sealed message and `open` will still "succeed", just against a different
//! (attacker-uncontrolled but unverified-as-original) keystream - a real tamper-evidence gap the
//! previous CCM-based construction did not have (CCM's B0 formatting block ties the nonce into its
//! CBC-MAC). Passing the nonce as AAD closes it using the construction's own designed mechanism
//! for authenticating out-of-band data, the same way a caller would bind a header to an AEAD tag.
//! Caught by `tampered_nonce_is_rejected` during this migration, not assumed - see `docs/DECISIONS.md`
//! D-63.
//!
//! # Provenance
//!
//! Inherits `hazmat::kalyna_gcm`'s own provisional status (D-56): not yet confirmed against the
//! primary DSTU 7624:2014 text, dual-oracle-cited (UAPKI + Bouncy Castle vectors) in the meantime -
//! unchanged by the CCM-to-GCM migration. `Kalyna256_256Gcm` was chosen over the other four
//! Kalyna-GCM variants as the sole construction here (256-bit key, matching the previous CCM
//! construction's key/nonce width exactly) - see D-51 for the fuller reasoning behind fixing one
//! variant rather than exposing all five, including why the `Strength`-enum precedent from
//! `crypto_pwhash` does not apply (a Kalyna variant is exactly the knob D-47 says to delete, not a
//! genuine per-context tradeoff the caller must make). The 16-byte tag (truncated from GCM's own
//! full 32-byte tag, via the same prefix-comparison convention `hazmat::kalyna_gcm`/`kalyna_gmac`
//! already support) matches the previous construction's tag length and libsodium's own
//! `crypto_secretbox` tag size - a fixed choice, not a new knob.
//!
//! # Example
//!
//! Encrypts a whole in-memory message under a freshly generated key. `seal`/`open` protect both
//! confidentiality (nobody without the key can read the message) and integrity (`open` rejects
//! anything tampered with, rather than returning wrong plaintext) - see below for the "tampered
//! ciphertext is rejected" case, `docs/TASKS.md` T-120's own required failure-path example.
//!
//! ```rust
//! use dstu_core::crypto_secretbox::{seal, open, SecretKey};
//!
//! let key = SecretKey::generate().expect("OS CSPRNG should not fail");
//! let sealed = seal(&key, b"message").expect("OS CSPRNG should not fail");
//! let opened = open(&key, &sealed).expect("authentic ciphertext");
//! assert_eq!(opened, b"message");
//!
//! // Tampering with the sealed blob (ciphertext, tag, or nonce) is detected, not silently
//! // "decrypted" into wrong plaintext.
//! let mut tampered = sealed.clone();
//! let last = tampered.len() - 1;
//! tampered[last] ^= 1;
//! assert!(open(&key, &tampered).is_err());
//! ```
use crate;
use crate;
use fmt;
use Zeroize;
const NONCE_LEN: usize = 32;
const TAG_LEN: usize = 16;
/// `crypto_secretbox` can fail for reasons beyond a wrong key.
/// A `crypto_secretbox` key. Always exactly 32 bytes - `Kalyna256_256Ccm`'s key length, this
/// module's one fixed construction (see the module doc).
;
/// Encrypts and authenticates `plaintext` under `key`, drawing a fresh random nonce internally.
/// Returns `nonce (32 bytes) || ciphertext (plaintext.len() bytes) || tag (16 bytes)` - no
/// message-length cap (see the module doc comment).
///
/// # Errors
///
/// Returns [`SecretboxError::Random`] if the OS CSPRNG fails - the only way this can fail.
/// Verifies and decrypts `sealed` (as produced by [`seal`]) under `key`.
///
/// # Errors
///
/// Returns [`SecretboxError::Truncated`] if `sealed` is shorter than a nonce plus a tag, or
/// [`SecretboxError::TagMismatch`] if authentication fails (wrong key, or `sealed` was tampered
/// with) - `sealed` is never partially trusted on a mismatch.