@amritk/nish-x86_64-linux 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/nish CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/nish-x86_64-linux",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "The nish native compiler for Linux on x86_64",
5
5
  "license": "MIT",
6
6
  "os": [
package/std/README.md CHANGED
@@ -96,6 +96,7 @@ is listed in [`SECURITY.md`](../SECURITY.md#threat-model).
96
96
  | [`crypto/jwt.ts`](./crypto/jwt.ts) | JSON Web Tokens in the JWS compact form. `JwtVerifier(maxToken)` holds its buffers, sized once, and `verify` / `verifyString(key, token, now)` answer `JWT_OK` or a `JWT_ERR_*` code: the key decides the algorithm (`alg` must name it exactly, so `none` and an HS header in front of an RSA key are refused), the header holds `alg` once and no `crit` or `b64`, base64url is strict, the signature is checked before the payload is read, and then `exp`, `nbf` and `iat` with a leeway, and `iss`, `aud` and `sub`, are checked. Keys: `jwtHmacKey` (HS256/384/512, keyed once through `HmacKey`), `jwtRsaKey` / `jwtRsaKeyParts` (RS256/384/512, 2048 bits or more), `jwtEcKey` (ES256, ES384), `jwtEd25519Key` (EdDSA), and the signing `jwtEs256SigningKey` / `jwtEd25519SigningKey`, each from a `Secret<u8[]>` it copies; `JwtKey.wipe()` zeroes the copy. `jwtSign` / `jwtSignString` / `jwtSignKid` mint a token under an HS, ES256 or EdDSA key. `peekKid` reads the unauthenticated `kid`, to choose a key | RFC 7515 Appendix A, RFC 7519, RFC 7518, RFC 8037 A.4, RFC 8725 |
97
97
  | [`crypto/ct.ts`](./crypto/ct.ts) | `timingSafeEqual(a, b)`, which reads every byte whatever it holds, and `timingSafeEqualAt(a, aOff, b, bOff, len)` over two windows, which answers `false` for a window outside its array. Two lengths that differ answer `false` at once, because a length is public, and so does an array longer than 2^31 − 1 bytes | — |
98
98
  | [`crypto/bcrypt.ts`](./crypto/bcrypt.ts) | `bcryptHash(password, salt, cost)`, the 60-character `$2b$` hash of a `Secret<u8[]>` password under a 16-byte salt and a cost of 4 to 31, and `bcryptVerify(password, hash)`, which reads `$2a$`, `$2b$` and `$2y$` hashes, refuses `$2x$`, and compares in constant time. The first 72 bytes of a password count, up to its first NUL, as under libxcrypt's `crypt_r` (where other implementations differ: [the interoperability table](../docs/security/crypto-bcrypt.md#interoperability)); the Blowfish state is wiped before either returns. `bcryptHash4(p0, p1, p2, p3, salts, cost)` and `bcryptVerify4(p0, p1, p2, p3, hashes)` do up to four at once, interleaved (four in the time of about 1.2 single hashes on one core), each lane the single path's answer; verify groups lanes by cost, and a malformed lane makes the whole call an `Err` naming it. `BCRYPT_MAX_PASSWORD`, `BCRYPT_HASH_LENGTH`, `BCRYPT_SALT_SIZE`, `BCRYPT_MIN_COST`, `BCRYPT_MAX_COST`. A `std/threads` task may call all four ([BCRYPT-9](../docs/security/crypto-bcrypt.md#findings)) | OpenBSD `bcrypt(3)`; crypt_blowfish 1.3 for `$2a$` and `$2y$` |
99
+ | [`crypto/bcrypt-batch.ts`](./crypto/bcrypt-batch.ts) | `bcryptBatcher({ width, holdNanos, cores, cost })`, a login batcher: `verify(password, hash, now)` and `hash(password, salt, now)` queue a request and answer its ticket, `waitNanos(now)` says when a run is due (a full batch, or the oldest request held `holdNanos`), and `run(now)` sends up to `width × cores` requests as `bcryptVerifyN` / `bcryptHashN` batches on one `std/threads` task per core and answers a `BcryptAnswer` (`ticket`, `ok`, `hash`, `error`) for each. It keeps and wipes its own copy of each password; a damaged stored hash answers its `Err` on its own row ([the record](../docs/security/crypto-bcrypt.md#the-login-batcher)) | — |
99
100
  | [`crypto/base64url.ts`](./crypto/base64url.ts) | `base64urlEncode(data)` and `base64urlDecode(text)`, unpadded; `base64StdPadEncode(data)` and `base64StdPadDecode(text)`, the padded standard alphabet that SCRAM and PEM use, constant-time in both directions. These are not `nish/base64`, which encodes only, is not constant-time and is for pixels; the names differ because std modules share the program's one namespace. Decoding is strict, so every byte string has one spelling. `base64urlDecode` answers `null` for any `=`, a character outside the alphabet, a length of 1 mod 4 or nonzero unused low bits; `base64StdPadDecode` requires the padding, answering `null` unless the text is whole four-character groups with `=` only as the last one or two characters, every other character in the alphabet and the unused bits zero. Both refuse a text longer than 2^31 − 1 bytes, and `base64urlEncode` panics on such an array | RFC 4648 §4, §5, §10 (`tests/link/std_base64_padded`) |
100
101
  | [`crypto/sha1.ts`](./crypto/sha1.ts) | `sha1(data)`, a fresh 20-byte digest, and `SHA1_SIZE` and `SHA1_BLOCK`. **Not for security**: SHA-1 is broken for collisions, and it is here only because RFC 6455 §4.2.2's `Sec-WebSocket-Accept` is defined on it (`nish/net/websocket` is its importer). One-shot only; it panics on an array longer than 2^31 − 1 bytes. It came after the #363 audit and has no record in `docs/security/` | FIPS 180-4 §6.1 |
101
102
  | [`crypto/x25519.ts`](./crypto/x25519.ts) | `x25519(scalar, u)`, the shared secret as a `Secret<u8[]>`, and `x25519Base(scalar)`, the public key, each from a `Secret<u8[]>` scalar, on ten 25.5-bit limbs in `i64`. Either answers `null` unless its arguments are `X25519_SIZE` (32) bytes; the scalar is clamped on a copy, which is wiped | RFC 7748 §5.2, §6.1, Wycheproof `x25519_test` |
@@ -0,0 +1,702 @@
1
+ /**
2
+ * `nish/crypto/bcrypt-batch` — a login batcher: it queues bcrypt verifies and
3
+ * hashes as they arrive, sends them as `bcryptVerifyN` / `bcryptHashN`
4
+ * batches, and runs the batches on `std/threads` tasks, one per core.
5
+ *
6
+ * import { bcryptCalibrate } from "nish/crypto/bcrypt"
7
+ * import { BcryptAnswer, BcryptBatcher, bcryptBatcher } from "nish/crypto/bcrypt-batch"
8
+ * import { availableParallelism, monotonicNanos } from "nish:process"
9
+ *
10
+ * const noHold: i64 = 0
11
+ * const made = bcryptBatcher({ width: bcryptCalibrate(), holdNanos: noHold, cores: availableParallelism(), cost: 10 })
12
+ * const batcher: BcryptBatcher = made.value
13
+ * // In the server's loop, for each login that arrives:
14
+ * const ticket: i32 = batcher.verify(password, storedHash, monotonicNanos())
15
+ * // ... wait at most batcher.waitNanos(now) for the next readiness event, then:
16
+ * for (const a of batcher.run(monotonicNanos())) { reply(a.ticket, a.ok) }
17
+ *
18
+ * **Why.** How full a batch is moves the time per password by more than any
19
+ * instruction-set work does: on one core a full six-lane cost-10 batch takes
20
+ * 15.7 ms per hash and a lone hash 56.9 ms (docs/security/crypto-bcrypt.md,
21
+ * "Speed"). A server whose logins arrive one at a time never fills a batch by
22
+ * itself; this module holds them until there are enough, or until a short
23
+ * window has passed.
24
+ *
25
+ * **When a batch is sent.** `run(now)` sends work when the queue holds at
26
+ * least `width` requests, or when the oldest has waited `holdNanos`; a hold
27
+ * of 0 sends whatever is queued at every `run`. It takes at most
28
+ * `width × cores` requests a run, the oldest first, and spreads them evenly
29
+ * over `min(cores, taken)` tasks, each running its share as one batch, so a
30
+ * run takes about one batch's time and the event loop gets control back
31
+ * between runs. Spreading rather than filling is deliberate: while a run is
32
+ * going nothing else can use the free cores, and a batch of two on each of
33
+ * three cores ends sooner than one of six on one. Under load the requests
34
+ * that arrive during a run fill the next one, so the batches fill on their
35
+ * own; at light load each request runs alone on a core, as a task per login
36
+ * would. `waitNanos(now)` says how long the loop may sleep before a `run` is
37
+ * due.
38
+ *
39
+ * **`run` blocks.** It opens a scope, runs the shares, one on the calling
40
+ * thread, and returns once every share has finished: about one batch, 60–95
41
+ * ms of cost 10 on the core this was measured on. That is the pause the
42
+ * password-hashing guide describes for a scope on the event loop's thread;
43
+ * give the batcher a thread of its own to avoid it.
44
+ *
45
+ * **The batcher keeps its own copy of each password.** `verify` and `hash`
46
+ * borrow the caller's `Secret<u8[]>`, as any function taking one does: they
47
+ * copy it, inside `exposeWith`, into a holder of the batcher's own (a class
48
+ * with one `readonly` `Secret` field), and the caller wipes its password as
49
+ * it would after `bcryptVerify`. `run` calls each copy's `wipe()` on the
50
+ * calling thread once the scope that read it has joined, so no task can
51
+ * still be reading it. A request that never runs (a batcher dropped with
52
+ * requests queued) is not wiped: call `run` until `pending()` is 0 before
53
+ * dropping one. The tasks call only `bcryptVerifyN`, `bcryptHashN` and
54
+ * `bcryptVerify`, which wipe everything they make
55
+ * (docs/security/crypto-bcrypt.md, BCRYPT-9, BCRYPT-11).
56
+ *
57
+ * **Every request gets its own answer.** A verify against a hash string that
58
+ * is not one makes `bcryptVerifyN` an `Err` for the whole batch; the share
59
+ * then verifies each of that batch's lanes alone, so only the damaged row
60
+ * answers the `Err` and the rest answer as they would have. A `hash` whose
61
+ * salt is not 16 bytes is answered with its `Err` at the next `run`, without
62
+ * being batched. The answers of a run come back in the order their requests
63
+ * were taken; match them to requests by `ticket`.
64
+ *
65
+ * Names carry the `bcryptBatch` prefix because a `std/` module's private
66
+ * functions share the importing program's flat symbol namespace
67
+ * (`docs/wp26-stdlib.md` §3e).
68
+ */
69
+
70
+ import {
71
+ BCRYPT_MAX_COST,
72
+ BCRYPT_MIN_COST,
73
+ bcryptHashN,
74
+ bcryptVerify,
75
+ bcryptVerifyN,
76
+ } from "nish/crypto/bcrypt"
77
+ import { Secret, expose, exposeWith, secret, wipe } from "nish:secret"
78
+ import { scope } from "nish/threads"
79
+
80
+ /** A request's kind: a verify of a password against a stored hash. */
81
+ const BCRYPT_BATCH_VERIFY: i32 = 0
82
+
83
+ /** A request's kind: a new hash of a password under a salt. */
84
+ const BCRYPT_BATCH_HASH: i32 = 1
85
+
86
+ /** The most lanes `bcryptVerifyN` and `bcryptHashN` take. */
87
+ const BCRYPT_BATCH_MAX_WIDTH: i32 = 6
88
+
89
+ /** How a batcher sends its work: see `bcryptBatcher`. */
90
+ export interface BcryptBatchConfig {
91
+ /** Lanes per batch, 1 to 6: `bcryptCalibrate()`'s answer. */
92
+ width: i32
93
+ /** How long the oldest request may wait for a batch to fill, in nanoseconds; 0 never waits. */
94
+ holdNanos: i64
95
+ /** How many tasks a run may use, at least 1: `availableParallelism()`. */
96
+ cores: i32
97
+ /** The cost `hash` requests are made at, 4 to 31. Verifies take theirs from the hash. */
98
+ cost: i32
99
+ }
100
+
101
+ /** What a request answered, under the ticket `verify` or `hash` gave it. */
102
+ export class BcryptAnswer {
103
+ /** The ticket of the request. */
104
+ ticket: i32
105
+ /** A verify: whether the password is the hash's. A hash: whether it was made. False with an error. */
106
+ ok: boolean
107
+ /** A hash request's `$2b$` hash; "" for a verify and for an error. */
108
+ hash: string
109
+ /** "" unless the request is an `Err`, as `bcryptVerify` or `bcryptHash` words it. */
110
+ error: string
111
+
112
+ constructor(ticket: i32, ok: boolean, hash: string, error: string) {
113
+ this.ticket = ticket
114
+ this.ok = ok
115
+ this.hash = hash
116
+ this.error = error
117
+ }
118
+ }
119
+
120
+ /**
121
+ * The batcher's copy of one queued password: a holder, so it may sit in the
122
+ * queue's array, made at the password's length and filled by `bcryptBatchTake`.
123
+ */
124
+ class BcryptBatchKey {
125
+ readonly key: Secret<u8[]>
126
+
127
+ constructor(length: i32) {
128
+ this.key = secret(new Array<u8>(length))
129
+ }
130
+
131
+ wipe(): void {
132
+ wipe(this.key)
133
+ }
134
+ }
135
+
136
+ /** A password's length, the one fact about it the batcher reads outside a batch. */
137
+ const bcryptBatchLengthOf = (bytes: u8[]): i32 => toI32(bytes.length)
138
+
139
+ /** Copies `from` into the exposed key `into`, which has its length. */
140
+ const bcryptBatchCopy = (into: u8[], from: u8[]): i32 => {
141
+ for (let i: i32 = 0; i < toI32(into.length) && i < toI32(from.length); i++) {
142
+ into[i] = from[i]
143
+ }
144
+ return 0
145
+ }
146
+
147
+ /** Copies the exposed password `bytes` into `held`'s key. */
148
+ const bcryptBatchFill = (bytes: u8[], held: BcryptBatchKey): i32 =>
149
+ exposeWith(held.key, bytes, bcryptBatchCopy)
150
+
151
+ /** A new holder with a copy of `password`; the caller keeps, and wipes, its own. */
152
+ const bcryptBatchTake = (password: Secret<u8[]>): BcryptBatchKey => {
153
+ const held: BcryptBatchKey = new BcryptBatchKey(expose(password, bcryptBatchLengthOf))
154
+ exposeWith(password, held, bcryptBatchFill)
155
+ return held
156
+ }
157
+
158
+ /**
159
+ * One task's work: requests in the order the share takes them. The task
160
+ * reads it and writes none of it; its answer is one string a request, "1"
161
+ * or "0" for a verify, the hash for a hash, and "!" and the error for an
162
+ * `Err`.
163
+ */
164
+ class BcryptBatchShare {
165
+ passwords: BcryptBatchKey[]
166
+ kinds: i32[]
167
+ /** A verify's stored hash, "" for a hash. */
168
+ hashes: string[]
169
+ /** 16 bytes a request; a verify's are zeros and are not read. */
170
+ salts: u8[]
171
+ cost: i32
172
+ width: i32
173
+
174
+ constructor(cost: i32, width: i32) {
175
+ this.passwords = []
176
+ this.kinds = []
177
+ this.hashes = []
178
+ this.salts = []
179
+ this.cost = cost
180
+ this.width = width
181
+ }
182
+ }
183
+
184
+ /** The holder at `list[i]` of `share`, or at `list[0]` past the batch's end (that lane is then passed as null). */
185
+ const bcryptBatchLane = (share: BcryptBatchShare, list: i32[], i: i32): BcryptBatchKey => {
186
+ const at: i32 = i < toI32(list.length) ? list[i] : list[0]
187
+ return share.passwords[at]
188
+ }
189
+
190
+ /** The answer string of one verify alone, for a lane of a batch that answered an `Err`. */
191
+ const bcryptBatchVerifyAlone = (held: BcryptBatchKey, hash: string): string => {
192
+ const said: Result<boolean, string> = bcryptVerify(held.key, hash)
193
+ if (said.isErr()) {
194
+ return `!${said.error}`
195
+ }
196
+ return said.value ? "1" : "0"
197
+ }
198
+
199
+ /**
200
+ * One `bcryptVerifyN` over the first `n` of `list`: a holder's field is
201
+ * passed only as a call's argument, so each lane count is its own call.
202
+ */
203
+ const bcryptBatchVerifyLanes = (
204
+ share: BcryptBatchShare,
205
+ list: i32[],
206
+ n: i32,
207
+ hashes: string[]
208
+ ): Result<boolean[], string> => {
209
+ const a: BcryptBatchKey = bcryptBatchLane(share, list, 0)
210
+ const b: BcryptBatchKey = bcryptBatchLane(share, list, 1)
211
+ const c: BcryptBatchKey = bcryptBatchLane(share, list, 2)
212
+ const d: BcryptBatchKey = bcryptBatchLane(share, list, 3)
213
+ const e: BcryptBatchKey = bcryptBatchLane(share, list, 4)
214
+ const f: BcryptBatchKey = bcryptBatchLane(share, list, 5)
215
+ if (n <= 1) {
216
+ return bcryptVerifyN(a.key, null, null, null, null, null, hashes)
217
+ }
218
+ if (n === 2) {
219
+ return bcryptVerifyN(a.key, b.key, null, null, null, null, hashes)
220
+ }
221
+ if (n === 3) {
222
+ return bcryptVerifyN(a.key, b.key, c.key, null, null, null, hashes)
223
+ }
224
+ if (n === 4) {
225
+ return bcryptVerifyN(a.key, b.key, c.key, d.key, null, null, hashes)
226
+ }
227
+ if (n === 5) {
228
+ return bcryptVerifyN(a.key, b.key, c.key, d.key, e.key, null, hashes)
229
+ }
230
+ return bcryptVerifyN(a.key, b.key, c.key, d.key, e.key, f.key, hashes)
231
+ }
232
+
233
+ /** One `bcryptHashN` over the first `n` of `list`, as `bcryptBatchVerifyLanes` is for verifies. */
234
+ const bcryptBatchHashLanes = (
235
+ share: BcryptBatchShare,
236
+ list: i32[],
237
+ n: i32,
238
+ salts: u8[]
239
+ ): Result<string[], string> => {
240
+ const a: BcryptBatchKey = bcryptBatchLane(share, list, 0)
241
+ const b: BcryptBatchKey = bcryptBatchLane(share, list, 1)
242
+ const c: BcryptBatchKey = bcryptBatchLane(share, list, 2)
243
+ const d: BcryptBatchKey = bcryptBatchLane(share, list, 3)
244
+ const e: BcryptBatchKey = bcryptBatchLane(share, list, 4)
245
+ const f: BcryptBatchKey = bcryptBatchLane(share, list, 5)
246
+ if (n <= 1) {
247
+ return bcryptHashN(a.key, null, null, null, null, null, salts, share.cost)
248
+ }
249
+ if (n === 2) {
250
+ return bcryptHashN(a.key, b.key, null, null, null, null, salts, share.cost)
251
+ }
252
+ if (n === 3) {
253
+ return bcryptHashN(a.key, b.key, c.key, null, null, null, salts, share.cost)
254
+ }
255
+ if (n === 4) {
256
+ return bcryptHashN(a.key, b.key, c.key, d.key, null, null, salts, share.cost)
257
+ }
258
+ if (n === 5) {
259
+ return bcryptHashN(a.key, b.key, c.key, d.key, e.key, null, salts, share.cost)
260
+ }
261
+ return bcryptHashN(a.key, b.key, c.key, d.key, e.key, f.key, salts, share.cost)
262
+ }
263
+
264
+ /** The verifies of `share` at `list[from..from + n)` as one `bcryptVerifyN`, each lane's answer into `out` at its index. */
265
+ const bcryptBatchVerifyRun = (
266
+ share: BcryptBatchShare,
267
+ list: i32[],
268
+ from: i32,
269
+ n: i32,
270
+ out: string[]
271
+ ): void => {
272
+ const lanes: i32[] = []
273
+ for (let i: i32 = from; i < from + n && i < toI32(list.length); i++) {
274
+ if (i >= 0) {
275
+ lanes.push(list[i])
276
+ }
277
+ }
278
+ if (toI32(lanes.length) === 0) {
279
+ return
280
+ }
281
+ const hashes: string[] = ["", "", "", "", "", ""]
282
+ for (let s: i32 = 0; s < toI32(lanes.length) && s < 6; s++) {
283
+ const at: i32 = lanes[s]
284
+ if (at >= 0 && at < toI32(share.hashes.length)) {
285
+ hashes[s] = share.hashes[at]
286
+ }
287
+ }
288
+ const said: Result<boolean[], string> = bcryptBatchVerifyLanes(share, lanes, toI32(lanes.length), hashes)
289
+ for (let s: i32 = 0; s < toI32(lanes.length); s++) {
290
+ const at: i32 = lanes[s]
291
+ let line: string = "0"
292
+ if (said.isErr()) {
293
+ // One damaged hash fails the whole call: answer each lane alone.
294
+ if (at >= 0 && at < toI32(share.passwords.length) && s < toI32(hashes.length)) {
295
+ line = bcryptBatchVerifyAlone(share.passwords[at], hashes[s])
296
+ }
297
+ } else {
298
+ const ok: boolean[] = said.value
299
+ line = s < toI32(ok.length) && ok[s] ? "1" : "0"
300
+ }
301
+ if (at >= 0 && at < toI32(out.length)) {
302
+ out[at] = line
303
+ }
304
+ }
305
+ }
306
+
307
+ /** The hashes of `share` at `list[from..from + n)` as one `bcryptHashN`, each lane's answer into `out` at its index. */
308
+ const bcryptBatchHashRun = (share: BcryptBatchShare, list: i32[], from: i32, n: i32, out: string[]): void => {
309
+ const lanes: i32[] = []
310
+ for (let i: i32 = from; i < from + n && i < toI32(list.length); i++) {
311
+ if (i >= 0) {
312
+ lanes.push(list[i])
313
+ }
314
+ }
315
+ if (toI32(lanes.length) === 0) {
316
+ return
317
+ }
318
+ const salts: u8[] = new Array<u8>(96)
319
+ for (let s: i32 = 0; s < toI32(lanes.length) && s < 6; s++) {
320
+ const at: i32 = lanes[s]
321
+ for (let j: i32 = 0; j < 16 && 16 * at + j < toI32(share.salts.length); j++) {
322
+ if (at >= 0) {
323
+ salts[16 * s + j] = share.salts[16 * at + j]
324
+ }
325
+ }
326
+ }
327
+ const made: Result<string[], string> = bcryptBatchHashLanes(share, lanes, toI32(lanes.length), salts)
328
+ for (let s: i32 = 0; s < toI32(lanes.length); s++) {
329
+ const at: i32 = lanes[s]
330
+ let line: string = ""
331
+ if (made.isErr()) {
332
+ line = `!${made.error}`
333
+ } else {
334
+ const all: string[] = made.value
335
+ line = s < toI32(all.length) ? all[s] : ""
336
+ }
337
+ if (at >= 0 && at < toI32(out.length)) {
338
+ out[at] = line
339
+ }
340
+ }
341
+ }
342
+
343
+ /** The indices of `share`'s requests of `kind`, in order. */
344
+ const bcryptBatchOfKind = (share: BcryptBatchShare, kind: i32): i32[] => {
345
+ const list: i32[] = []
346
+ for (let i: i32 = 0; i < toI32(share.kinds.length); i++) {
347
+ if (share.kinds[i] === kind) {
348
+ list.push(i)
349
+ }
350
+ }
351
+ return list
352
+ }
353
+
354
+ /**
355
+ * One task: every request of `share`, the verifies and then the hashes, in
356
+ * batches of up to `width`. It reads `share` and writes only arrays it made,
357
+ * and answers one line a request, joined by "\n": a task hands back a string,
358
+ * not an array of them. No answer holds a newline: a `$2b$` hash is base-64
359
+ * and bcrypt's errors are one line.
360
+ */
361
+ const bcryptBatchShareRun = (share: BcryptBatchShare): string => {
362
+ const out: string[] = []
363
+ for (let i: i32 = 0; i < toI32(share.kinds.length); i++) {
364
+ out.push("")
365
+ }
366
+ const width: i32 = share.width < 1 ? 1 : share.width
367
+ const verifies: i32[] = bcryptBatchOfKind(share, BCRYPT_BATCH_VERIFY)
368
+ for (let i: i32 = 0; i < toI32(verifies.length); i += width) {
369
+ bcryptBatchVerifyRun(share, verifies, i, width, out)
370
+ }
371
+ const hashes: i32[] = bcryptBatchOfKind(share, BCRYPT_BATCH_HASH)
372
+ for (let i: i32 = 0; i < toI32(hashes.length); i += width) {
373
+ bcryptBatchHashRun(share, hashes, i, width, out)
374
+ }
375
+ return out.join("\n")
376
+ }
377
+
378
+ /**
379
+ * The cost a verify's stored hash names, read from its fourth and fifth
380
+ * characters, or -1 when they are not two digits; only the order of a run
381
+ * reads it, so that one cost's verifies share a batch.
382
+ */
383
+ const bcryptBatchCostOf = (hash: string): i32 => {
384
+ if (toI32(hash.length) < 7) {
385
+ return -1
386
+ }
387
+ const tens: i32 = toI32(hash.charCodeAt(4)) - 48
388
+ const ones: i32 = toI32(hash.charCodeAt(5)) - 48
389
+ if (tens < 0 || tens > 9 || ones < 0 || ones > 9) {
390
+ return -1
391
+ }
392
+ return tens * 10 + ones
393
+ }
394
+
395
+ /** The lines of a task's answer, split at "\n". */
396
+ const bcryptBatchLines = (joined: string): string[] => {
397
+ const lines: string[] = []
398
+ let from: i32 = 0
399
+ while (from >= 0 && from <= toI32(joined.length)) {
400
+ const at: i32 = toI32(joined.indexOf("\n", from))
401
+ if (at < from || at > toI32(joined.length)) {
402
+ lines.push(joined.slice(from))
403
+ return lines
404
+ }
405
+ lines.push(joined.slice(from, at))
406
+ from = at + 1
407
+ }
408
+ return lines
409
+ }
410
+
411
+ /** The answer a share's string stands for. */
412
+ const bcryptBatchAnswerOf = (ticket: i32, kind: i32, said: string): BcryptAnswer => {
413
+ if (said.startsWith("!")) {
414
+ return new BcryptAnswer(ticket, false, "", said.substring(1))
415
+ }
416
+ if (kind === BCRYPT_BATCH_HASH) {
417
+ return new BcryptAnswer(ticket, said !== "", said, "")
418
+ }
419
+ return new BcryptAnswer(ticket, said === "1", "", "")
420
+ }
421
+
422
+ /**
423
+ * A queue of bcrypt requests and the policy that sends them: made by
424
+ * `bcryptBatcher`. `verify` and `hash` queue, `run` sends and answers,
425
+ * `waitNanos` says when the next `run` is due.
426
+ */
427
+ export class BcryptBatcher {
428
+ holdNanos: i64
429
+ /** The queue, from `head`: each request's copy of its password, kind, stored hash, salt (16 bytes each), ticket and arrival. */
430
+ passwords: BcryptBatchKey[]
431
+ kinds: i32[]
432
+ hashes: string[]
433
+ salts: u8[]
434
+ tickets: i32[]
435
+ arrived: i64[]
436
+ /** Answers made without a batch (a salt of the wrong length), handed out by the next `run`. */
437
+ early: BcryptAnswer[]
438
+ width: i32
439
+ cores: i32
440
+ cost: i32
441
+ head: i32
442
+ /** The next ticket. */
443
+ next: i32
444
+
445
+ constructor(config: BcryptBatchConfig) {
446
+ this.width = config.width
447
+ this.holdNanos = config.holdNanos
448
+ this.cores = config.cores
449
+ this.cost = config.cost
450
+ this.passwords = []
451
+ this.kinds = []
452
+ this.hashes = []
453
+ this.salts = []
454
+ this.tickets = []
455
+ this.arrived = []
456
+ this.head = 0
457
+ this.early = []
458
+ this.next = 0
459
+ }
460
+
461
+ /** How many requests are queued or answered and not yet handed out by `run`. */
462
+ pending(): i32 {
463
+ return toI32(this.kinds.length) - this.head + toI32(this.early.length)
464
+ }
465
+
466
+ /**
467
+ * Queues a verify of `password` against `hash`, which arrived at `now`
468
+ * (`monotonicNanos()`), and answers its ticket. The batcher queues a copy
469
+ * of `password` and wipes the copy once the verify has run; the caller
470
+ * wipes its own.
471
+ */
472
+ verify(password: Secret<u8[]>, hash: string, now: i64): i32 {
473
+ const ticket: i32 = this.next
474
+ this.next = this.next + 1
475
+ this.enqueue(password, BCRYPT_BATCH_VERIFY, hash, new Array<u8>(16), ticket, now)
476
+ return ticket
477
+ }
478
+
479
+ /**
480
+ * Queues a new hash of `password` under `salt` (16 bytes, fresh from
481
+ * `crypto.getRandomValues`) at the batcher's cost, which arrived at `now`,
482
+ * and answers its ticket. The batcher queues a copy of `password` and
483
+ * wipes the copy once the hash is made; a salt of another length is
484
+ * answered with its `Err` by the next `run`, and no copy is made.
485
+ */
486
+ hash(password: Secret<u8[]>, salt: u8[], now: i64): i32 {
487
+ const ticket: i32 = this.next
488
+ this.next = this.next + 1
489
+ if (toI32(salt.length) !== 16) {
490
+ this.early.push(new BcryptAnswer(ticket, false, "", "bcrypt: the salt is not 16 bytes"))
491
+ return ticket
492
+ }
493
+ this.enqueue(password, BCRYPT_BATCH_HASH, "", salt, ticket, now)
494
+ return ticket
495
+ }
496
+
497
+ /** Appends one request to the queue. */
498
+ enqueue(password: Secret<u8[]>, kind: i32, hash: string, salt: u8[], ticket: i32, now: i64): void {
499
+ this.passwords.push(bcryptBatchTake(password))
500
+ this.kinds.push(kind)
501
+ this.hashes.push(hash)
502
+ for (let j: i32 = 0; j < 16 && j < toI32(salt.length); j++) {
503
+ this.salts.push(salt[j])
504
+ }
505
+ this.tickets.push(ticket)
506
+ this.arrived.push(now)
507
+ }
508
+
509
+ /**
510
+ * Nanoseconds from `now` until a `run` is due: 0 when it is due now (a full
511
+ * batch is queued, the oldest request has waited `holdNanos`, or an answer
512
+ * is waiting), -1 when nothing is queued.
513
+ */
514
+ waitNanos(now: i64): i64 {
515
+ if (toI32(this.early.length) > 0) {
516
+ return 0
517
+ }
518
+ const queued: i32 = toI32(this.kinds.length) - this.head
519
+ if (queued <= 0) {
520
+ return -1
521
+ }
522
+ if (queued >= this.width || this.head >= toI32(this.arrived.length)) {
523
+ return 0
524
+ }
525
+ const waited: i64 = now - this.arrived[this.head]
526
+ return waited >= this.holdNanos ? 0 : this.holdNanos - waited
527
+ }
528
+
529
+ /**
530
+ * Sends what is due and answers it: nothing (bar answers made without a
531
+ * batch) until `waitNanos(now)` is 0, then up to `width × cores` of the
532
+ * oldest requests, spread evenly over up to `cores` tasks, each running
533
+ * its share as batches of up to `width`. Returns once every task has
534
+ * finished, with the copy of each password it ran wiped.
535
+ */
536
+ run(now: i64): BcryptAnswer[] {
537
+ const answers: BcryptAnswer[] = []
538
+ for (const a of this.early) {
539
+ answers.push(a)
540
+ }
541
+ this.early = []
542
+ if (this.waitNanos(now) !== 0) {
543
+ return answers
544
+ }
545
+ const queued: i32 = toI32(this.kinds.length) - this.head
546
+ if (queued <= 0) {
547
+ return answers
548
+ }
549
+ const most: i32 = this.width * this.cores
550
+ const taken: i32 = queued < most ? queued : most
551
+ const order: i32[] = this.orderOf(taken)
552
+ const tasks: i32 = taken < this.cores ? taken : this.cores
553
+ const shares: BcryptBatchShare[] = []
554
+ // Share k takes order[taken·k/tasks, taken·(k+1)/tasks): contiguous, so one cost's verifies stay together.
555
+ const owner: i32[] = []
556
+ for (let k: i32 = 0; k < tasks; k++) {
557
+ const share: BcryptBatchShare = new BcryptBatchShare(this.cost, this.width)
558
+ const lo: i32 = (taken * k) / tasks
559
+ const hi: i32 = (taken * (k + 1)) / tasks
560
+ for (let i: i32 = lo; i >= 0 && i < hi && i < toI32(order.length); i++) {
561
+ const at: i32 = order[i]
562
+ if (at >= 0 && at < toI32(this.passwords.length) && at < toI32(this.kinds.length)) {
563
+ share.passwords.push(this.passwords[at])
564
+ share.kinds.push(this.kinds[at])
565
+ share.hashes.push(at < toI32(this.hashes.length) ? this.hashes[at] : "")
566
+ for (let j: i32 = 0; j < 16; j++) {
567
+ share.salts.push(16 * at + j < toI32(this.salts.length) ? this.salts[16 * at + j] : toU8(0))
568
+ }
569
+ owner.push(at)
570
+ }
571
+ }
572
+ shares.push(share)
573
+ }
574
+ const said: string[] = []
575
+ for (let k: i32 = 0; k < tasks; k++) {
576
+ said.push("")
577
+ }
578
+ {
579
+ using s = scope()
580
+ for (let k: i32 = 0; k < toI32(shares.length); k++) {
581
+ s.spawn(bcryptBatchShareRun, shares[k], said, k)
582
+ }
583
+ }
584
+ let flat: i32 = 0
585
+ for (let k: i32 = 0; k < toI32(said.length); k++) {
586
+ const one: string[] = bcryptBatchLines(said[k])
587
+ for (let i: i32 = 0; i < toI32(one.length) && flat < toI32(owner.length); i++) {
588
+ const at: i32 = owner[flat]
589
+ flat++
590
+ if (at >= 0 && at < toI32(this.tickets.length) && at < toI32(this.kinds.length)) {
591
+ answers.push(bcryptBatchAnswerOf(this.tickets[at], this.kinds[at], one[i]))
592
+ }
593
+ }
594
+ }
595
+ for (const at of order) {
596
+ if (at >= 0 && at < toI32(this.passwords.length)) {
597
+ const held: BcryptBatchKey = this.passwords[at]
598
+ held.wipe()
599
+ }
600
+ }
601
+ this.head = this.head + taken
602
+ this.compact()
603
+ return answers
604
+ }
605
+
606
+ /**
607
+ * The queue indices of the `taken` oldest requests, verifies by the cost
608
+ * their hash names and then hashes, each group in arrival order.
609
+ */
610
+ orderOf(taken: i32): i32[] {
611
+ const keyed: i32[] = []
612
+ const keys: i32[] = []
613
+ for (let i: i32 = this.head; i < this.head + taken && i < toI32(this.kinds.length); i++) {
614
+ keyed.push(i)
615
+ let key: i32 = 1000
616
+ if (this.kinds[i] !== BCRYPT_BATCH_HASH) {
617
+ key = i < toI32(this.hashes.length) ? bcryptBatchCostOf(this.hashes[i]) : -1
618
+ }
619
+ keys.push(key)
620
+ }
621
+ // Stable: one pass a distinct key, lowest first, each in arrival order.
622
+ // A run takes at most 36 requests, of a few costs.
623
+ const sorted: i32[] = []
624
+ let last: i32 = -2
625
+ while (toI32(sorted.length) < toI32(keyed.length)) {
626
+ let low: i32 = 1001
627
+ for (const key of keys) {
628
+ if (key > last && key < low) {
629
+ low = key
630
+ }
631
+ }
632
+ if (low > 1000) {
633
+ return sorted
634
+ }
635
+ for (let i: i32 = 0; i < toI32(keyed.length) && i < toI32(keys.length); i++) {
636
+ if (keys[i] === low) {
637
+ sorted.push(keyed[i])
638
+ }
639
+ }
640
+ last = low
641
+ }
642
+ return sorted
643
+ }
644
+
645
+ /** Drops the requests before `head` once they are most of the queue, so a long-lived batcher's arrays stay small. */
646
+ compact(): void {
647
+ if (this.head < 64 || this.head * 2 < toI32(this.kinds.length)) {
648
+ return
649
+ }
650
+ const passwords: BcryptBatchKey[] = []
651
+ const kinds: i32[] = []
652
+ const hashes: string[] = []
653
+ const salts: u8[] = []
654
+ const tickets: i32[] = []
655
+ const arrived: i64[] = []
656
+ for (let i: i32 = this.head; i < toI32(this.kinds.length); i++) {
657
+ if (
658
+ i < toI32(this.passwords.length) &&
659
+ i < toI32(this.hashes.length) &&
660
+ i < toI32(this.tickets.length) &&
661
+ i < toI32(this.arrived.length)
662
+ ) {
663
+ passwords.push(this.passwords[i])
664
+ kinds.push(this.kinds[i])
665
+ hashes.push(this.hashes[i])
666
+ for (let j: i32 = 0; j < 16; j++) {
667
+ salts.push(16 * i + j < toI32(this.salts.length) ? this.salts[16 * i + j] : toU8(0))
668
+ }
669
+ tickets.push(this.tickets[i])
670
+ arrived.push(this.arrived[i])
671
+ }
672
+ }
673
+ this.passwords = passwords
674
+ this.kinds = kinds
675
+ this.hashes = hashes
676
+ this.salts = salts
677
+ this.tickets = tickets
678
+ this.arrived = arrived
679
+ this.head = 0
680
+ }
681
+ }
682
+
683
+ /**
684
+ * A new batcher for `config`, or an `Err` naming the field out of range: a
685
+ * width outside 1 to 6, a negative hold, fewer than one core, or a cost
686
+ * outside 4 to 31.
687
+ */
688
+ export const bcryptBatcher = (config: BcryptBatchConfig): Result<BcryptBatcher, string> => {
689
+ if (config.width < 1 || config.width > BCRYPT_BATCH_MAX_WIDTH) {
690
+ return Err("bcrypt: the batch width is outside 1 to 6")
691
+ }
692
+ if (config.holdNanos < 0) {
693
+ return Err("bcrypt: the hold is negative")
694
+ }
695
+ if (config.cores < 1) {
696
+ return Err("bcrypt: a batcher needs at least one core")
697
+ }
698
+ if (config.cost < BCRYPT_MIN_COST || config.cost > BCRYPT_MAX_COST) {
699
+ return Err("bcrypt: the cost is outside 4 to 31")
700
+ }
701
+ return Ok(new BcryptBatcher(config))
702
+ }