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
//! `crypto_stream` equivalent (`docs/dstu-crypto-project.md` "Mapping onto the libsodium API",
//! `docs/TASKS.md` roadmap Step 3 item 3, `docs/DECISIONS.md` D-67) - a libsodium-ergonomics wrapper over
//! [`hazmat::strumok::Strumok256`](crate::hazmat::strumok::Strumok256).
//!
//! # No authentication whatsoever
//!
//! Strumok is a bare keystream generator - XOR-ing it into a message provides confidentiality
//! only, never integrity (`hazmat::strumok`'s own module doc, `docs/release-readiness.md`'s
//! "Streaming audio, confidentiality only" use-case row). Unlike [`crate::crypto_secretbox`],
//! [`decrypt`] **never fails on tampered input** - it has no tag to check, so a modified
//! `sealed` value decrypts to different, silently-wrong plaintext instead of an error, the same
//! documented no-integrity-by-design property `hazmat::kalyna_xts` already has
//! (`tests/kalyna_xts.rs`'s `tampered_ciphertext_does_not_error_but_produces_garbage`). This is
//! why this module's functions are named `encrypt`/`decrypt`, not `seal`/`open` -
//! `crypto_secretbox` reserves `seal`/`open` specifically to signal "this authenticates," and
//! this primitive does not. Callers needing integrity must wrap each message in
//! [`crate::crypto_secretbox`] (or a chunked `crypto_secretstream`, once T-40 exists) instead of,
//! or on top of, this module - never rely on this module alone where tamper-detection matters.
//!
//! # Hidden IV
//!
//! Confirmed with the project owner (roadmap Step 3 item 3 was left as an explicit open fork,
//! unlike this roadmap's other named forks): the IV is generated internally from the OS CSPRNG,
//! the same choice `crypto_secretbox` made for its nonce (D-51), never caller-supplied. This
//! matters more here than for most primitives: `hazmat::strumok`'s own module doc carries a
//! "never reuse the same key+IV pair" warning, backed by a dedicated test
//! (`reusing_key_and_iv_leaks_plaintext_xor`, `docs/TASKS.md` T-103) pinning the catastrophic two-time-
//! pad property directly - reusing a key+IV pair XORs the two plaintexts together, recoverable
//! without ever breaking the cipher itself. Hiding IV generation removes that footgun from the
//! caller's surface entirely, at the cost of matching libsodium's own lower-level
//! `crypto_stream_xor(c, m, mlen, n, k)` C signature (`n` is a caller-supplied parameter there) -
//! a deliberate divergence, not an oversight, matching `crypto_secretbox`'s own precedent of
//! prioritizing misuse-resistance over raw-API parity (D-47's tie-breaker).
//!
//! # Variant
//!
//! Only `Strumok256` is exposed here (D-47's "delete the knob", matching `crypto_auth`/
//! `crypto_kdf`'s single-256-bit-variant choice, D-66) - `Strumok512` stays `hazmat`-only.
//!
//! # Provenance
//!
//! Inherits `hazmat::strumok`'s own D-18 status: vectors are UAPKI-attributed, not confirmed
//! against the primary DSTU 8845:2019 text.
//!
//! # Example
//!
//! Confidentiality only, **no integrity** (see the "No authentication whatsoever" section above) -
//! prefer [`crate::crypto_secretbox`]/[`crate::crypto_secretstream`] unless you specifically need a
//! bare keystream cipher and are handling authentication yourself. Note what tampering does here,
//! in contrast to `crypto_secretbox`'s example above: `decrypt` never errors, it just returns
//! different, silently-wrong plaintext.
//!
//! ```rust
//! use dstu_core::crypto_stream::{encrypt, decrypt, Key};
//!
//! let key = Key::generate().expect("OS CSPRNG should not fail");
//! let sealed = encrypt(&key, b"message").expect("OS CSPRNG should not fail");
//! let opened = decrypt(&key, &sealed).expect("sealed is at least IV-length");
//! assert_eq!(opened, b"message");
//!
//! // Tampering is not detected - decrypt "succeeds" with garbage plaintext instead of erroring.
//! let mut tampered = sealed.clone();
//! let last = tampered.len() - 1;
//! tampered[last] ^= 1;
//! let garbage = decrypt(&key, &tampered).expect("still at least IV-length, so still Ok");
//! assert_ne!(garbage, b"message");
//! ```
use crateStrumok256;
use crate;
use fmt;
use Zeroize;
const IV_LEN: usize = 32;
/// `crypto_stream` can fail only if the OS CSPRNG fails while generating a fresh IV - there is no
/// tag to mismatch (see the module doc's "No authentication" section).
/// A `crypto_stream` key. Always exactly 32 bytes - `Strumok256`'s key length (see the module
/// doc).
;
/// XORs `plaintext` with a fresh keystream under `key`, drawing a random IV internally. Returns
/// `iv (32 bytes) || ciphertext (plaintext.len() bytes)` - no authentication (see the module
/// doc's "No authentication" section).
///
/// # Errors
///
/// Returns [`StreamError::Random`] if the OS CSPRNG fails - the only way this can fail.
/// Reverses [`encrypt`] under `key` - XOR is its own inverse
/// ([`hazmat::strumok`](crate::hazmat::strumok)'s `apply_keystream`), so this recovers the
/// original plaintext bit-for-bit when `sealed` is exactly [`encrypt`]'s own output. **Never
/// fails on tampered input** - see the module doc's "No authentication" section; a modified
/// `sealed` decrypts to different, silently-wrong plaintext, not an error.
///
/// # Errors
///
/// Returns [`StreamError::Truncated`] if `sealed` is shorter than an IV (32 bytes) - the only
/// possible error, since there is no tag to fail.