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
//! Fisher-Yates permutation of embedding positions, seeded by the STC key.
//!
//! # Why the positions are permuted at all
//!
//! Embedding in raster order concentrates every change in the first rows of the
//! image that happen to be cheap, which is a spatial signature a steganalyst can
//! read off a residual plot without any statistics at all. Visiting the pixels
//! in a secret order spreads the changes over the whole container, and — more
//! importantly — makes the set of touched positions unpredictable to anyone who
//! does not hold the key.
//!
//! # Why a stream cipher rather than an RNG
//!
//! The permutation must be reproduced exactly by the extraction path, which sees
//! nothing but the stego image and the password. A general purpose RNG gives no
//! guarantee that two builds, two platforms or two versions of a crate produce
//! the same stream; ChaCha20 is specified bit by bit, so the same seed yields
//! the same keystream everywhere and forever. Determinism here is a correctness
//! requirement, not a convenience: a single differing draw permutes the tail of
//! the sequence and the payload is lost.
//!
//! # Why rejection sampling
//!
//! Reducing a uniform 64-bit draw with `value % bound` is biased whenever
//! `bound` does not divide the size of the draw space: the low residues occur
//! once more often than the high ones. The bias is tiny per swap, but it applies
//! to every position of a multi-megapixel image and it is *structured* — it
//! favours the same residues for everyone, which is precisely the kind of
//! regularity an attacker can accumulate over many images. Draws landing in the
//! incomplete final block are discarded instead, leaving an exactly uniform
//! index at the price of a repeat that happens with probability below
//! `bound / 2^64`.
use ;
use ChaCha20;
use ZeroizeOnDrop;
/// Nonce of the keystream that drives the shuffle.
///
/// Fixed at zero on purpose. A nonce exists to keep two messages encrypted
/// under the same key apart, and there is only ever one "message" per key here:
/// the single keystream consumed by one shuffle. Uniqueness is already provided
/// by the seed, which HKDF derives per container from a master key that is
/// itself salted with the image's perceptual hash.
const PERMUTATION_NONCE: = ;
/// Bytes of keystream produced per refill.
///
/// Drawing eight bytes at a time straight from the cipher would call into it
/// once per swap — tens of millions of times on a large container. A buffer of
/// whole ChaCha20 blocks amortises that to one call per 64 blocks. The size is a
/// whole multiple of [`DRAW_BYTES`], which is what lets [`KeystreamReader`]
/// assume a draw never straddles the end of the buffer.
const KEYSTREAM_BUFFER_BYTES: usize = 4096;
/// Bytes consumed by one draw: a `u64` read little-endian.
const DRAW_BYTES: usize = 8;
/// Buffered reader over the ChaCha20 keystream of one permutation.
///
/// Wiped on drop: the keystream is a direct function of the STC seed, so a copy
/// of it left in freed memory is as good as a copy of the seed for anyone who
/// wants to reconstruct the embedding order.
/// Builds the secret visiting order of `n_pixels` embedding positions.
///
/// The result is a permutation of `0..n_pixels`: every index appears exactly
/// once, so the caller can walk it end to end and touch each position at most
/// once. The same `stc_seed` and the same `n_pixels` always produce the same
/// permutation, on every platform and every build — the extraction path depends
/// on it.
///
/// # Algorithm
///
/// Fisher-Yates, walking down from the last index. At step `i` the draw is an
/// unbiased index into `0..=i`, obtained by rejection sampling over the
/// keystream as described in the module documentation.
pub