@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 +0 -0
- package/package.json +1 -1
- package/std/README.md +1 -0
- package/std/crypto/bcrypt-batch.ts +702 -0
package/bin/nish
CHANGED
|
Binary file
|
package/package.json
CHANGED
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
|
+
}
|