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
//! The hashlock preimage derivation (SPEC_ms_hashlock §2).
//!
//! THE RULE LIVES HERE, in the codec, beside the kind that carries its
//! output: one crate, one corpus, one SHA pin, one provenance pin for the Go
//! port. `ms hashlock` is a thin verb over these four functions.
//!
//! Two methods, the operator's choice (brainstorm L5): `preimage_hardened`
//! is PBKDF2-HMAC-SHA256 with a fixed salt, 100,000 iterations and dkLen 32
//! (L4); `preimage_sha256` is one SHA-256 of the phrase bytes. Both take the
//! phrase as BYTES, exactly as given -- no trimming, folding or normalising
//! happens here or in any caller (§4.3). `digest` is SHA-256 of X, the value
//! the policy carries; it is public the moment the policy is engraved and is
//! therefore NOT zeroized.
//!
//! THE SALT IS FIXED AND HAS NO PARAMETER (L13). Changing it after any vector
//! ships is a new method, not a tweak: every engraved policy's preimage was
//! derived under this exact byte string.
use pbkdf2_hmac;
use ;
use Zeroizing;
use crate;
/// The fixed salt (ASCII, copyable by hand, domain-separated from BIP-39's
/// `"mnemonic"` and from `me`'s 16-byte random seal salt).
pub const HASHLOCK_SALT: & = b"ms-hashlock-v1";
/// PBKDF2 iteration count -- the operator's cap, chosen so a signer at a
/// tenth of the SH2's measured rate still derives in reasonable time.
pub const HASHLOCK_ITERATIONS: u32 = 100_000;
/// Derived-key length: a miniscript `sha256(H)` preimage is exactly 32 bytes.
pub const HASHLOCK_DKLEN: usize = 32;
/// X = PBKDF2-HMAC-SHA256(phrase, HASHLOCK_SALT, HASHLOCK_ITERATIONS, 32).
/// X = SHA-256(phrase). The brainwallet construction; the CLI warns on it at
/// every length (L12) and this function does not judge.
/// X from the OS CSPRNG, failing closed: an error, never a zeroed buffer.
/// Lives here rather than in the CLI so the whole preimage surface -- and its
/// randomness contract -- is one crate's (R0 r0 correctness I-2).
/// H = SHA-256(X): what the policy carries and the plate shows. Public.
// ─── The PHRASE RULE (SPEC_ms_hashlock §4.3) ────────────────────────────────
//
// IT LIVES HERE, IN THE CODEC, for the reason the module header already gives
// for the derivation: one crate, one corpus, one SHA pin, one provenance pin
// for the Go port. Until H6 the rule was `ms-cli`'s `validate_phrase`, private
// to that binary; `me sysw pack`'s `phrase:` record must apply the SAME rule
// byte for byte (SPEC_hashlock_H6 §3.1) and `me` depends on `ms-codec`, not on
// `ms-cli`. Leaving it where it was would have produced a THIRD copy of a rule
// whose whole point is that the host and the device cannot disagree about what
// a phrase is.
//
// `ms-cli`'s `validate_phrase` now delegates here and keeps only its own
// message rendering, so there is still exactly one implementation.
/// The phrase cap. Its own constant on each side, lockstep-pinned; NOT the
/// device's plate-legibility `passphrase.MaxLen`.
pub const HASHLOCK_PHRASE_MAX_CHARS: usize = 100;
/// The shortest string `looks_like_ms1` will call ms1-shaped.
const MIN_MS1_LEN: usize = 48;
const BECH32_CHARSET: &str = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
/// Why a phrase was refused. One variant per rule, in the order the rule
/// checks them; the CALLER renders the sentence.
/// `looks_like_ms1` over the NORMALISED token: trimmed, ASCII-lowercased,
/// display separators (whitespace, `-`, `,`) stripped, then at least 48
/// characters, an `ms1` prefix and only bech32 characters.
///
/// NO CHECKSUM, deliberately. A GROUPED plate is what `ms hashlock`'s
/// engraving card prints and therefore what an operator retypes, and a
/// checksum test would answer false for it — so the guard would miss the one
/// spelling it exists to catch.
/// The rule. ORDER MATTERS and is the spec's: empty, printable ASCII,
/// ms1-shape (BEFORE the cap, so a grouped plate string gets the `--in`
/// remedy and not "too long"), the cap, 64-hex.
///
/// It changes nothing: no trim, no case fold, no normalisation. The shape test
/// works on a copy.
/// The QR text a hashlock PHRASE plate carries (SPEC_hashlock_H6 §8.6), byte
/// for byte: three labelled lines, LF-separated, NO trailing newline, the
/// phrase LAST.
///
/// The phrase is last so a reader knows where it ends: it may itself contain
/// `:` and spaces, and everything after `phrase: ` on the final line is the
/// phrase, verbatim, with real `0x20` spaces.
///
/// The method line names the ALGORITHM in full — not the `--method` selector —
/// so a reader with the plate and no tool can reproduce the derivation. Its
/// parameters are read from `HASHLOCK_SALT`, `HASHLOCK_ITERATIONS` and
/// `HASHLOCK_DKLEN` and never from a literal, so a parameter change cannot
/// leave the plate lying.
///
/// `hashlock v1` is the VERSION TAG of this TEXT, not of the derivation. A
/// future parameter set gets `hashlock v2`.
///
/// IT RETURNS `Zeroizing<String>` BECAUSE THE PHRASE IS IN IT. Every other
/// phrase-bearing value in this workspace is protected -- `read_phrase_from`
/// and `read_phrase_stdin` return `Zeroizing<Vec<u8>>`, `preimage_hardened`
/// and `preimage_sha256` return `Zeroizing<[u8; 32]>`, and the kind carries
/// `Payload::Preimage(Zeroizing<[u8; 32]>)` -- and a plain `String` here would
/// have been the one hole in that surface, holding the phrase in the clear on
/// the heap until the allocator happened to reuse the page.
///
/// **The buffer is `Zeroizing` from the FIRST byte, and it is allocated once.**
/// Wrapping a finished `format!` would be no protection at all: the `format!`
/// would build an unprotected `String` containing the phrase and the wrap would
/// only guard the copy. The exact capacity is reserved up front so no `push_str`
/// can reallocate and abandon an unwiped buffer part-way through. The `method`
/// line is deliberately NOT protected -- it is three compile-time constants and
/// carries nothing of the phrase.