@amritk/nish-x86_64-linux 0.17.0 → 0.20.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.
Files changed (61) hide show
  1. package/INSTALL.md +43 -25
  2. package/bin/nish +0 -0
  3. package/package.json +2 -2
  4. package/runtime/nish.d.ts +55 -5
  5. package/runtime/nish.h +231 -20
  6. package/runtime/nish.mjs +16 -0
  7. package/runtime/runtime-aes.c +490 -0
  8. package/runtime/runtime-cross.c +366 -0
  9. package/runtime/runtime-host.c +7 -6
  10. package/runtime/runtime-net.c +93 -34
  11. package/runtime/runtime-os.c +213 -15
  12. package/runtime/runtime-parallel.c +590 -107
  13. package/runtime/runtime-simd.c +203 -0
  14. package/runtime/runtime-wasm.c +16 -3
  15. package/runtime/runtime.c +89 -23
  16. package/runtime/shim.mjs +71 -1
  17. package/scripts/build.sh +225 -20
  18. package/std/README.md +86 -55
  19. package/std/bytes.ts +163 -0
  20. package/std/crypto/LICENSE-libsecp256k1 +19 -0
  21. package/std/crypto/aes.ts +238 -76
  22. package/std/crypto/chacha20poly1305.ts +43 -9
  23. package/std/crypto/ed25519.ts +840 -0
  24. package/std/crypto/hkdf.ts +304 -12
  25. package/std/crypto/hmac.ts +245 -53
  26. package/std/crypto/p256.ts +1376 -42
  27. package/std/crypto/x25519.ts +55 -37
  28. package/std/crypto/x509-random.ts +71 -0
  29. package/std/crypto/x509.ts +9 -9
  30. package/std/json-tokenizer.ts +1296 -0
  31. package/std/json-typed.ts +1014 -0
  32. package/std/json-value.ts +1163 -0
  33. package/std/json.ts +1412 -70
  34. package/std/net/buffer-pool.ts +151 -0
  35. package/std/net/hpack.ts +416 -112
  36. package/std/net/http-fields.ts +93 -0
  37. package/std/net/http1-server.ts +417 -55
  38. package/std/net/http1.ts +31 -6
  39. package/std/net/http2-tls.ts +112 -24
  40. package/std/net/http2.ts +333 -75
  41. package/std/net/http3-server.ts +231 -45
  42. package/std/net/http3.ts +187 -28
  43. package/std/net/prefork.ts +412 -0
  44. package/std/net/quic-conn-params.ts +86 -16
  45. package/std/net/quic-listener.ts +576 -219
  46. package/std/net/quic-packet.ts +443 -58
  47. package/std/net/quic-recovery.ts +361 -30
  48. package/std/net/quic-stream.ts +434 -28
  49. package/std/net/quic.ts +922 -206
  50. package/std/net/router.ts +676 -0
  51. package/std/net/tls/codec.ts +426 -27
  52. package/std/net/tls/record-server.ts +478 -132
  53. package/std/net/tls/record.ts +133 -42
  54. package/std/net/tls/schedule.ts +464 -55
  55. package/std/net/tls/ticket.ts +610 -0
  56. package/std/net/tls-tcp.ts +204 -57
  57. package/std/net/tls.ts +1132 -305
  58. package/std/net/url.ts +571 -0
  59. package/std/random.ts +94 -0
  60. package/std/text.ts +77 -0
  61. package/std/threads.ts +438 -14
@@ -5,14 +5,23 @@
5
5
  * Initial secrets, AEAD packet protection, header protection and the Retry
6
6
  * integrity tag.
7
7
  *
8
- * import { quicInitialSecrets, quicKeys, quicParseHeader, quicOpenPacket,
8
+ * import { quicInitialSecrets, quicKeys, quicParseHeader, quicOpenPacket, QuicKeysSlot,
9
9
  * QUIC_AEAD_AES_128_GCM, QUIC_PACKET_OK } from "nish/net/quic-packet";
10
+ * import { Secret, secret, wipe } from "nish:secret";
10
11
  *
11
12
  * const secrets = quicInitialSecrets(clientDcid); // RFC 9001 §5.2
12
- * const keys = quicKeys(QUIC_AEAD_AES_128_GCM, secrets.client);
13
+ * if (secrets === null) { … a DCID over 20 bytes … }
14
+ * const client: u8[] = new Array<u8>(secrets.client.length); // public, so a copy
15
+ * client.set(secrets.client);
16
+ * const held: Secret<u8[]> = secret(client);
17
+ * const slot = new QuicKeysSlot(); // ours, so ours to wipe
18
+ * const keys = quicKeys(slot, QUIC_AEAD_AES_128_GCM, held);
19
+ * wipe(held);
20
+ * if (keys === null) { … }
13
21
  * const header = quicParseHeader(datagram, 0, 8); // 8: our short-header DCID length
14
22
  * const packet = quicOpenPacket(keys, datagram, header, largestReceived);
15
23
  * if (packet.error === QUIC_PACKET_OK) { … packet.payload holds the frames … }
24
+ * slot.wipe(); // once the level is done
16
25
  *
17
26
  * The module is sans-IO: bytes in, bytes out, no socket and no clock. It
18
27
  * frames and protects packets and stops there, so it knows nothing of frames,
@@ -48,14 +57,25 @@
48
57
  * carries its own Length, and `QuicHeader.end` is where the next packet in the
49
58
  * datagram starts. A short header has none, so it runs to the datagram's end.
50
59
  *
51
- * **Secrets are not wiped.** `QuicKeys` holds a packet key, an IV and a
52
- * header-protection key, and `quicKeys`, `quicKeyUpdateSecret` and the seal
53
- * and open functions hold traffic secrets and keys in arena memory until that
54
- * memory is reused. `nish/crypto`'s signers and X25519 now hold their keys as
55
- * a `Secret` from `nish:secret` and wipe them (ECC-2, X509-7); this module
56
- * does not yet, and `docs/security/quic.md` records that as QUIC-1. The
57
- * Initial keys are not secret, because anyone who reads the client's first
58
- * DCID can derive them (RFC 9001 §5.2); the Handshake and 1-RTT keys are.
60
+ * **Secrets.** `QuicKeys` holds a packet key, an IV and a header-protection
61
+ * key, pointing into a `QuicKeysSlot`'s arrays. `quicKeys` and
62
+ * `quicKeysUpdate` take a traffic secret as a `Secret` from `nish:secret`
63
+ * and a slot the caller owns, and derive into the slot; a slot the caller
64
+ * owns is the caller's to wipe, and `QuicKeysSlot.wipe` zeroes it with
65
+ * stores no optimiser removes, AES schedules included (QUIC-1).
66
+ * `quicKeyUpdateSecret` answers the next secret as a `Secret`, and every
67
+ * derivation wipes its HKDF intermediates before it returns. `quicKeysWipe`
68
+ * zeroes the arrays one `QuicKeys` points at. The Initial keys are not
69
+ * secret, because anyone who reads the client's first DCID can derive them
70
+ * (RFC 9001 §5.2), so `quicInitialSecrets` answers plain arrays, and
71
+ * `quicKeysInto`, `quicKeysUpdateInto`, `quicKeyUpdateSecretInto` and
72
+ * `quicInitialSecretsInto` derive from plain secrets over a caller's
73
+ * `HkdfScratch`; the Handshake and 1-RTT keys are secret. Each derivation's
74
+ * own temporaries go with its arena block. A slot whose traffic secret is
75
+ * held as a `Secret` may also derive with `quicKeysHeldInto`, and ratchet
76
+ * the secret in place with `quicKeyUpdateSecretHeldInto`. Sealing and
77
+ * opening a packet wipe the nonce, which gives the IV away, and the
78
+ * header-protection mask once each is used (QUIC-2).
59
79
  *
60
80
  * **Constant time.** The AEADs and the header-protection masks are
61
81
  * `nish/crypto/aes` and `nish/crypto/chacha20poly1305`, with their guarantees.
@@ -73,10 +93,31 @@
73
93
  * private functions share the importing program's flat symbol namespace
74
94
  * (`docs/wp26-stdlib.md` §3e).
75
95
  */
76
- import { aesGcmOpen, aesGcmSeal, aesHeaderMask, aesKey, AesKey } from "nish/crypto/aes"
96
+ import { Secret, expose, wipe } from "nish:secret"
97
+ import {
98
+ aesGcmOpen,
99
+ aesGcmSeal,
100
+ aesHeaderMask,
101
+ aesKey,
102
+ aesKeyInto,
103
+ aesKeyWipe,
104
+ AesKey,
105
+ } from "nish/crypto/aes"
77
106
  import { chacha20HeaderMask, chacha20Poly1305Open, chacha20Poly1305Seal } from "nish/crypto/chacha20poly1305"
78
107
  import { timingSafeEqual } from "nish/crypto/ct"
79
- import { hkdfExpandLabelSha256, hkdfExpandLabelSha384, hkdfExtractSha256 } from "nish/crypto/hkdf"
108
+ import {
109
+ HkdfScratch,
110
+ hkdfExpandLabelHeldInto,
111
+ hkdfExpandLabelInto,
112
+ hkdfExpandLabelSha256,
113
+ hkdfExpandLabelSha256Held,
114
+ hkdfExpandLabelSha256Secret,
115
+ hkdfExpandLabelSha384,
116
+ hkdfExpandLabelSha384Held,
117
+ hkdfExpandLabelSha384Secret,
118
+ hkdfExtractInto,
119
+ hkdfExtractSha256,
120
+ } from "nish/crypto/hkdf"
80
121
 
81
122
  /** QUIC version 1 (RFC 9000 §15), the only version this module protects. */
82
123
  export const QUIC_VERSION_1: i64 = 1
@@ -964,6 +1005,28 @@ const quicPacketSecretLength = (aead: i32): i32 => (aead === QUIC_AEAD_AES_256_G
964
1005
  const quicPacketSecretFits = (aead: i32, secret: u8[]): boolean =>
965
1006
  quicPacketKeyLength(aead) !== 0 && toI32(secret.length) === quicPacketSecretLength(aead)
966
1007
 
1008
+ /** `quicPacketSecretFits` for a held secret, whose length is public, read through `expose`. */
1009
+ const quicPacketHeldFits = (aead: i32, trafficSecret: Secret<u8[]>): boolean =>
1010
+ quicPacketKeyLength(aead) !== 0 &&
1011
+ expose(trafficSecret, quicPacketBytesLength) === quicPacketSecretLength(aead)
1012
+
1013
+ /** The length of `bytes`, for `expose` to read off a held secret. */
1014
+ const quicPacketBytesLength = (bytes: u8[]): i32 => toI32(bytes.length)
1015
+
1016
+ /** A plain copy of `bytes`, for `expose` to hand a derived key to the `QuicKeys` that holds it. */
1017
+ const quicPacketDup = (bytes: u8[]): u8[] => quicPacketSlice(bytes, 0, toI32(bytes.length))
1018
+
1019
+ /**
1020
+ * `quicPacketExpandLabel` of a held secret, answered held: the
1021
+ * `hkdfExpandLabelSha*Secret` form, which wipes its own intermediates.
1022
+ */
1023
+ const quicPacketExpandHeld = (aead: i32, prk: Secret<u8[]>, label: string, length: i32): Secret<u8[]> => {
1024
+ const none: u8[] = []
1025
+ return aead === QUIC_AEAD_AES_256_GCM
1026
+ ? hkdfExpandLabelSha384Secret(prk, label, none, length)
1027
+ : hkdfExpandLabelSha256Secret(prk, label, none, length)
1028
+ }
1029
+
967
1030
  /**
968
1031
  * Whether `keys` has the shape `quicKeys` gives: a known AEAD, key and
969
1032
  * header-protection key of its length, a 12-byte IV, and for the AES suites
@@ -985,65 +1048,362 @@ const quicPacketKeysUsable = (keys: QuicKeys): boolean => {
985
1048
  }
986
1049
 
987
1050
  /**
988
- * Keys for `aead` with the packet key and IV derived from `secret` (RFC 9001
989
- * §5.1) and the header-protection key given, which a key update keeps
990
- * (§6). The caller has checked `secret` with `quicPacketSecretFits`.
1051
+ * The packet-protection keys of RFC 9001 §5.1 from a traffic secret, held as
1052
+ * a `Secret` the caller still owns and wipes, derived into `slot`, which the
1053
+ * caller owns too: the AEAD key (`quic key`), the IV (`quic iv`) and the
1054
+ * header-protection key (`quic hp`), each HKDF-Expand-Label with an empty
1055
+ * context under the hash `aead` pairs with, written into the slot's arrays
1056
+ * and the AES schedules expanded there. The secret is read through a plain
1057
+ * copy inside an arena block, which is zeroed, and the derivation's
1058
+ * temporaries go with the block, so the call keeps in the arena only the
1059
+ * `HkdfScratch` it makes for the derivation, which wipes itself before the
1060
+ * derivation returns.
1061
+ * Answers the slot's keys, which the next derivation into it overwrites and
1062
+ * `QuicKeysSlot.wipe` zeroes, or `null`, deriving nothing, for an `aead`
1063
+ * this module does not know or a secret that is not that hash's length. The
1064
+ * Initial secrets are public (RFC 9001 §5.2), so a caller deriving Initial
1065
+ * keys wraps a copy of one in `secret`, or uses `quicKeysInto`.
991
1066
  */
992
- const quicPacketDerive = (aead: i32, secret: u8[], hp: u8[], hpAes: AesKey | null): QuicKeys => {
993
- const keys: QuicKeys = new QuicKeys(
994
- aead,
995
- quicPacketExpandLabel(aead, secret, "quic key", quicPacketKeyLength(aead)),
996
- quicPacketExpandLabel(aead, secret, "quic iv", QUIC_IV_SIZE),
997
- hp
998
- )
999
- keys.hpAes = hpAes
1000
- if (aead !== QUIC_AEAD_CHACHA20_POLY1305) {
1001
- keys.packetAes = aesKey(keys.key)
1067
+ export const quicKeys = (slot: QuicKeysSlot, aead: i32, trafficSecret: Secret<u8[]>): QuicKeys | null => {
1068
+ if (!quicPacketHeldFits(aead, trafficSecret)) {
1069
+ return null
1002
1070
  }
1003
- return keys
1071
+ const scratch: HkdfScratch = new HkdfScratch()
1072
+ {
1073
+ using _scope = arena()
1074
+ const plain: u8[] = expose(trafficSecret, quicPacketDup)
1075
+ quicKeysInto(scratch, slot, aead, plain)
1076
+ secureZero(plain)
1077
+ }
1078
+ return slot.keys
1079
+ }
1080
+
1081
+ /**
1082
+ * The next generation's traffic secret for a key update (RFC 9001 §6.1):
1083
+ * HKDF-Expand-Label(`trafficSecret`, `quic ku`, "", Hash.length), taken and
1084
+ * answered as a `Secret` the caller wipes. The header-protection key does
1085
+ * not change with it (§6), so the new keys are `quicKeysUpdate` of the old
1086
+ * keys and this. Answers `null` where `quicKeys` would.
1087
+ */
1088
+ export const quicKeyUpdateSecret = (aead: i32, trafficSecret: Secret<u8[]>): Secret<u8[]> | null => {
1089
+ if (!quicPacketHeldFits(aead, trafficSecret)) {
1090
+ return null
1091
+ }
1092
+ return quicPacketExpandHeld(aead, trafficSecret, "quic ku", quicPacketSecretLength(aead))
1004
1093
  }
1005
1094
 
1006
1095
  /**
1007
- * The packet-protection keys of RFC 9001 §5.1 from a traffic `secret`: the
1008
- * AEAD key (`quic key`), the IV (`quic iv`) and the header-protection key
1009
- * (`quic hp`), each HKDF-Expand-Label with an empty context under the hash
1010
- * `aead` pairs with. Answers `null` for an `aead` this module does not know,
1011
- * or a secret that is not that hash's length.
1096
+ * The next generation's keys in a key update (RFC 9001 §6), derived into
1097
+ * `slot`, which the caller owns and which must not be the slot `keys` live
1098
+ * in: the packet key and IV from `nextSecret`, which `quicKeyUpdateSecret`
1099
+ * made from the current secret and the caller still owns, read through a
1100
+ * plain copy inside an arena block that is zeroed, and `keys`'
1101
+ * header-protection key kept, since header protection never changes with
1102
+ * the phase. The new generation holds its own copy of that key and its own
1103
+ * schedule, in the slot, so wiping one generation leaves the other usable.
1104
+ * Answers the slot's keys, or `null`, deriving nothing, for keys `quicKeys`
1105
+ * did not make, keys that live in `slot`, or a secret of the wrong length.
1012
1106
  */
1013
- export const quicKeys = (aead: i32, secret: u8[]): QuicKeys | null => {
1014
- if (!quicPacketSecretFits(aead, secret)) {
1107
+ export const quicKeysUpdate = (
1108
+ slot: QuicKeysSlot,
1109
+ keys: QuicKeys,
1110
+ nextSecret: Secret<u8[]>
1111
+ ): QuicKeys | null => {
1112
+ if (keys === slot.keys || !quicPacketKeysUsable(keys) || !quicPacketHeldFits(keys.aead, nextSecret)) {
1015
1113
  return null
1016
1114
  }
1017
- const hp: u8[] = quicPacketExpandLabel(aead, secret, "quic hp", quicPacketKeyLength(aead))
1018
- return quicPacketDerive(aead, secret, hp, aead === QUIC_AEAD_CHACHA20_POLY1305 ? null : aesKey(hp))
1115
+ quicPacketPoint(slot, keys.aead)
1116
+ const next: QuicKeys = slot.keys
1117
+ next.hp.set(keys.hp)
1118
+ const scratch: HkdfScratch = new HkdfScratch()
1119
+ {
1120
+ using _scope = arena()
1121
+ const hpAes: AesKey | null = next.hpAes
1122
+ if (hpAes !== null) {
1123
+ aesKeyInto(next.hp, hpAes)
1124
+ }
1125
+ const plain: u8[] = expose(nextSecret, quicPacketDup)
1126
+ quicPacketDeriveInto(scratch, next, plain)
1127
+ secureZero(plain)
1128
+ }
1129
+ return next
1019
1130
  }
1020
1131
 
1021
1132
  /**
1022
- * The next generation's traffic secret for a key update (RFC 9001 §6.1):
1023
- * HKDF-Expand-Label(`secret`, `quic ku`, "", Hash.length). The
1024
- * header-protection key does not change with it (§6), so the new keys are
1025
- * `quicKeysUpdate` of the old keys and this. Answers `null` where `quicKeys`
1026
- * would.
1133
+ * Zeroes every array `keys` holds: the packet key, the IV and the
1134
+ * header-protection key with `secureZero`, and each AES schedule with
1135
+ * `aesKeyWipe`, whose volatile stores reach the whole of `roundKeys` (the
1136
+ * kernels' powers of H included) and H itself. It zeroes the arrays `keys`
1137
+ * points at now; a slot's arrays of the other lengths are
1138
+ * `QuicKeysSlot.wipe`'s, which zeroes them all.
1139
+ */
1140
+ export const quicKeysWipe = (keys: QuicKeys): void => {
1141
+ secureZero(keys.key)
1142
+ secureZero(keys.iv)
1143
+ secureZero(keys.hp)
1144
+ const packetAes: AesKey | null = keys.packetAes
1145
+ if (packetAes !== null) {
1146
+ aesKeyWipe(packetAes)
1147
+ }
1148
+ const hpAes: AesKey | null = keys.hpAes
1149
+ if (hpAes !== null) {
1150
+ aesKeyWipe(hpAes)
1151
+ }
1152
+ }
1153
+
1154
+ // ---- Keys in a connection slot ----------------------------------------------
1155
+
1156
+ /**
1157
+ * The storage one direction of one level's packet keys lives in for a
1158
+ * connection slot (QUIC-3): a key and a header-protection key of each
1159
+ * length, the IV, and an AES key schedule of each size for each, made once,
1160
+ * and the `QuicKeys` that `quicKeysInto` and `quicKeysUpdateInto` point at
1161
+ * the ones the negotiated AEAD uses. So installing a level's keys, or a key
1162
+ * update's, writes into memory the slot already holds and keeps nothing in
1163
+ * the arena. The slot is its owner's to wipe (QUIC-1): `wipe` zeroes all of
1164
+ * it.
1165
+ */
1166
+ export class QuicKeysSlot {
1167
+ /** The keys, pointing into the arrays below; `aead` is 0 until something is derived. */
1168
+ keys: QuicKeys
1169
+ key16: u8[]
1170
+ key32: u8[]
1171
+ hp16: u8[]
1172
+ hp32: u8[]
1173
+ packet128: AesKey
1174
+ packet256: AesKey
1175
+ hp128: AesKey
1176
+ hp256: AesKey
1177
+
1178
+ constructor() {
1179
+ this.key16 = new Array<u8>(16)
1180
+ this.key32 = new Array<u8>(32)
1181
+ this.hp16 = new Array<u8>(16)
1182
+ this.hp32 = new Array<u8>(32)
1183
+ this.keys = new QuicKeys(0, this.key16, new Array<u8>(QUIC_IV_SIZE), this.hp16)
1184
+ this.packet128 = new AesKey(10, new Array<u64>(88))
1185
+ this.packet256 = new AesKey(14, new Array<u64>(120))
1186
+ this.hp128 = new AesKey(10, new Array<u64>(88))
1187
+ this.hp256 = new AesKey(14, new Array<u64>(120))
1188
+ }
1189
+
1190
+ /**
1191
+ * Zeroes every key the slot holds: the keys and the IV with `secureZero`,
1192
+ * and each AES schedule with `aesKeyWipe`, whose volatile stores reach the
1193
+ * whole of `roundKeys` (the kernels' powers of H included) and H itself.
1194
+ */
1195
+ wipe(): void {
1196
+ secureZero(this.key16)
1197
+ secureZero(this.key32)
1198
+ secureZero(this.hp16)
1199
+ secureZero(this.hp32)
1200
+ secureZero(this.keys.iv)
1201
+ aesKeyWipe(this.packet128)
1202
+ aesKeyWipe(this.packet256)
1203
+ aesKeyWipe(this.hp128)
1204
+ aesKeyWipe(this.hp256)
1205
+ }
1206
+ }
1207
+
1208
+ /** Points `slot.keys` at the arrays `aead` needs: the packet key and its schedule, and the header-protection ones. */
1209
+ const quicPacketPoint = (slot: QuicKeysSlot, aead: i32): void => {
1210
+ const keys: QuicKeys = slot.keys
1211
+ const wide: boolean = quicPacketKeyLength(aead) === 32
1212
+ keys.aead = aead
1213
+ keys.key = wide ? slot.key32 : slot.key16
1214
+ keys.hp = wide ? slot.hp32 : slot.hp16
1215
+ keys.packetAes = null
1216
+ keys.hpAes = null
1217
+ if (aead !== QUIC_AEAD_CHACHA20_POLY1305) {
1218
+ keys.packetAes = wide ? slot.packet256 : slot.packet128
1219
+ keys.hpAes = wide ? slot.hp256 : slot.hp128
1220
+ }
1221
+ }
1222
+
1223
+ /**
1224
+ * `quicPacketDerive` in place: the packet key and IV from `secret` into
1225
+ * `keys`' own arrays, and for the AES suites the key's schedule expanded
1226
+ * into its own `AesKey`. HKDF runs in `kdf`, which wipes what it held, and
1227
+ * the expansion's temporaries go with the arena block.
1228
+ */
1229
+ const quicPacketDeriveInto = (kdf: HkdfScratch, keys: QuicKeys, secret: u8[]): void => {
1230
+ const aead: i32 = keys.aead
1231
+ const hashLength: i32 = quicPacketSecretLength(aead)
1232
+ using _scope = arena()
1233
+ const none: u8[] = []
1234
+ hkdfExpandLabelInto(kdf, hashLength, secret, "quic key", none, 0, 0, keys.key, 0, quicPacketKeyLength(aead))
1235
+ hkdfExpandLabelInto(kdf, hashLength, secret, "quic iv", none, 0, 0, keys.iv, 0, QUIC_IV_SIZE)
1236
+ const packetAes: AesKey | null = keys.packetAes
1237
+ if (packetAes !== null) {
1238
+ aesKeyInto(keys.key, packetAes)
1239
+ }
1240
+ }
1241
+
1242
+ /**
1243
+ * `quicKeys` into `slot`: the packet key, IV and header-protection key of
1244
+ * RFC 9001 §5.1 derived from `secret` straight into the slot's arrays, in
1245
+ * `kdf`, with the AES schedules expanded in place. Answers the slot's keys,
1246
+ * or `null`, deriving nothing, where `quicKeys` answers `null`. The keys
1247
+ * are the slot's: the next derivation into it overwrites them.
1027
1248
  */
1028
- export const quicKeyUpdateSecret = (aead: i32, secret: u8[]): u8[] | null => {
1249
+ export const quicKeysInto = (
1250
+ kdf: HkdfScratch,
1251
+ slot: QuicKeysSlot,
1252
+ aead: i32,
1253
+ secret: u8[]
1254
+ ): QuicKeys | null => {
1029
1255
  if (!quicPacketSecretFits(aead, secret)) {
1030
1256
  return null
1031
1257
  }
1032
- return quicPacketExpandLabel(aead, secret, "quic ku", quicPacketSecretLength(aead))
1258
+ quicPacketPoint(slot, aead)
1259
+ const keys: QuicKeys = slot.keys
1260
+ quicPacketDeriveInto(kdf, keys, secret)
1261
+ {
1262
+ using _scope = arena()
1263
+ const none: u8[] = []
1264
+ hkdfExpandLabelInto(
1265
+ kdf,
1266
+ quicPacketSecretLength(aead),
1267
+ secret,
1268
+ "quic hp",
1269
+ none,
1270
+ 0,
1271
+ 0,
1272
+ keys.hp,
1273
+ 0,
1274
+ quicPacketKeyLength(aead)
1275
+ )
1276
+ const hpAes: AesKey | null = keys.hpAes
1277
+ if (hpAes !== null) {
1278
+ aesKeyInto(keys.hp, hpAes)
1279
+ }
1280
+ }
1281
+ return keys
1033
1282
  }
1034
1283
 
1035
1284
  /**
1036
- * The next generation's keys in a key update (RFC 9001 §6): the packet key
1037
- * and IV derived from `nextSecret`, which `quicKeyUpdateSecret` made from the
1038
- * current secret, and `keys`' header-protection key kept, expanded as it was,
1039
- * since header protection never changes with the phase. Answers `null` for
1040
- * keys `quicKeys` did not make or a secret of the wrong length.
1285
+ * `quicKeysUpdate` into `slot`, which must not be the slot `keys` live in:
1286
+ * the next generation's packet key and IV from `nextSecret`, and `keys`'
1287
+ * header-protection key shared, as every generation shares it (RFC 9001
1288
+ * §6). Answers the slot's keys, or `null` where `quicKeysUpdate` does.
1041
1289
  */
1042
- export const quicKeysUpdate = (keys: QuicKeys, nextSecret: u8[]): QuicKeys | null => {
1290
+ export const quicKeysUpdateInto = (
1291
+ kdf: HkdfScratch,
1292
+ slot: QuicKeysSlot,
1293
+ keys: QuicKeys,
1294
+ nextSecret: u8[]
1295
+ ): QuicKeys | null => {
1043
1296
  if (!quicPacketKeysUsable(keys) || !quicPacketSecretFits(keys.aead, nextSecret)) {
1044
1297
  return null
1045
1298
  }
1046
- return quicPacketDerive(keys.aead, nextSecret, keys.hp, keys.hpAes)
1299
+ quicPacketPoint(slot, keys.aead)
1300
+ const next: QuicKeys = slot.keys
1301
+ next.hp = keys.hp
1302
+ next.hpAes = keys.hpAes
1303
+ quicPacketDeriveInto(kdf, next, nextSecret)
1304
+ return next
1305
+ }
1306
+
1307
+ /**
1308
+ * `quicKeyUpdateSecret` into `out`, which must be exactly the secret's
1309
+ * length and not `secret` itself. Answers whether it derived, which it does
1310
+ * where `quicKeyUpdateSecret` answers a secret.
1311
+ */
1312
+ export const quicKeyUpdateSecretInto = (kdf: HkdfScratch, aead: i32, secret: u8[], out: u8[]): boolean => {
1313
+ const length: i32 = quicPacketSecretLength(aead)
1314
+ if (!quicPacketSecretFits(aead, secret) || toI32(out.length) !== length) {
1315
+ return false
1316
+ }
1317
+ using _scope = arena()
1318
+ const none: u8[] = []
1319
+ hkdfExpandLabelInto(kdf, length, secret, "quic ku", none, 0, 0, out, 0, length)
1320
+ return true
1321
+ }
1322
+
1323
+ /**
1324
+ * `quicKeysInto` from a traffic secret held as a `Secret`: the packet key, IV
1325
+ * and header-protection key derived from it straight into the slot's arrays,
1326
+ * the plain output of each derivation wiped, and the AES schedules expanded
1327
+ * in place inside an arena block. Answers the slot's keys, or `null`,
1328
+ * deriving nothing, where `quicKeys` answers `null`.
1329
+ */
1330
+ export const quicKeysHeldInto = (slot: QuicKeysSlot, aead: i32, secret: Secret<u8[]>): QuicKeys | null => {
1331
+ if (!quicPacketHeldFits(aead, secret)) {
1332
+ return null
1333
+ }
1334
+ quicPacketPoint(slot, aead)
1335
+ const keys: QuicKeys = slot.keys
1336
+ const hashLength: i32 = quicPacketSecretLength(aead)
1337
+ const keyLength: i32 = quicPacketKeyLength(aead)
1338
+ const none: u8[] = []
1339
+ hkdfExpandLabelHeldInto(hashLength, secret, "quic key", none, keys.key, 0, keyLength)
1340
+ hkdfExpandLabelHeldInto(hashLength, secret, "quic iv", none, keys.iv, 0, QUIC_IV_SIZE)
1341
+ hkdfExpandLabelHeldInto(hashLength, secret, "quic hp", none, keys.hp, 0, keyLength)
1342
+ using _scope = arena()
1343
+ const packetAes: AesKey | null = keys.packetAes
1344
+ if (packetAes !== null) {
1345
+ aesKeyInto(keys.key, packetAes)
1346
+ }
1347
+ const hpAes: AesKey | null = keys.hpAes
1348
+ if (hpAes !== null) {
1349
+ aesKeyInto(keys.hp, hpAes)
1350
+ }
1351
+ return keys
1352
+ }
1353
+
1354
+ /**
1355
+ * `quicKeyUpdateSecret` from one held secret into another (RFC 9001 §6.1):
1356
+ * HKDF-Expand-Label(`src`, `quic ku`, "", Hash.length) written over `dst`'s
1357
+ * bytes in place, under SHA-384 for a 48-byte secret and SHA-256 for a
1358
+ * 32-byte one. `dst` may be `src`, which ratchets it. Answers whether it
1359
+ * derived: not unless both hold 32 bytes or both 48.
1360
+ */
1361
+ export const quicKeyUpdateSecretHeldInto = (dst: Secret<u8[]>, src: Secret<u8[]>): boolean => {
1362
+ const length: i32 = expose(src, quicPacketBytesLength)
1363
+ if ((length !== 32 && length !== 48) || expose(dst, quicPacketBytesLength) !== length) {
1364
+ return false
1365
+ }
1366
+ const none: u8[] = []
1367
+ if (length === 48) {
1368
+ hkdfExpandLabelSha384Held(dst, src, "quic ku", none)
1369
+ } else {
1370
+ hkdfExpandLabelSha256Held(dst, src, "quic ku", none)
1371
+ }
1372
+ return true
1373
+ }
1374
+
1375
+ /**
1376
+ * `quicInitialSecrets` of the Destination Connection ID `buf[at .. at +
1377
+ * length)`, read in place, into `client` and `server`, 32 bytes each.
1378
+ * Answers whether it derived: not for an ID over 20 bytes, a window outside
1379
+ * `buf`, or an output of another length.
1380
+ */
1381
+ export const quicInitialSecretsInto = (
1382
+ kdf: HkdfScratch,
1383
+ buf: u8[],
1384
+ at: i32,
1385
+ length: i32,
1386
+ client: u8[],
1387
+ server: u8[]
1388
+ ): boolean => {
1389
+ if (
1390
+ length < 0 ||
1391
+ length > QUIC_MAX_CID_LENGTH ||
1392
+ at < 0 ||
1393
+ at > toI32(buf.length) - length ||
1394
+ toI32(client.length) !== 32 ||
1395
+ toI32(server.length) !== 32
1396
+ ) {
1397
+ return false
1398
+ }
1399
+ using _scope = arena()
1400
+ const none: u8[] = []
1401
+ const initial: u8[] = new Array<u8>(32)
1402
+ hkdfExtractInto(kdf, 32, quicPacketInitialSalt(), quicPacketSlice(buf, at, at + length), initial, 0)
1403
+ hkdfExpandLabelInto(kdf, 32, initial, "client in", none, 0, 0, client, 0, 32)
1404
+ hkdfExpandLabelInto(kdf, 32, initial, "server in", none, 0, 0, server, 0, 32)
1405
+ secureZero(initial)
1406
+ return true
1047
1407
  }
1048
1408
 
1049
1409
  /** The bits of the first byte header protection covers: four in a long header, five in a short one (RFC 9001 §5.4.1). */
@@ -1152,12 +1512,17 @@ export const quicSealInPlace = (
1152
1512
  const payloadAt: i32 = start + headerLength
1153
1513
  {
1154
1514
  using _scope = arena()
1515
+ // The nonce is the IV with a public number XORed in, so it gives the IV
1516
+ // away, and the mask is the header-protection key's keystream: both are
1517
+ // wiped once used, before the arena block takes them back.
1518
+ const nonce: u8[] = quicPacketNonce(keys, packetNumber)
1155
1519
  const sealed: u8[] | null = quicPacketSeal(
1156
1520
  keys,
1157
- quicPacketNonce(keys, packetNumber),
1521
+ nonce,
1158
1522
  quicPacketSlice(buf, start, payloadAt),
1159
1523
  quicPacketSlice(buf, payloadAt, payloadAt + payloadLength)
1160
1524
  )
1525
+ wipe(nonce)
1161
1526
  if (sealed === null || payloadAt + toI32(sealed.length) > toI32(buf.length)) {
1162
1527
  return -1
1163
1528
  }
@@ -1167,15 +1532,22 @@ export const quicSealInPlace = (
1167
1532
  keys,
1168
1533
  quicPacketSlice(buf, sampleAt, sampleAt + QUIC_SAMPLE_SIZE)
1169
1534
  )
1170
- if (mask === null || toI32(mask.length) < 5) {
1535
+ if (mask === null) {
1171
1536
  return -1
1172
1537
  }
1173
- buf[start] = toU8(first ^ (toI32(mask[0]) & quicPacketProtectedBits(first)))
1174
- for (let k: i32 = 0; k < pnLength && k + 1 < toI32(mask.length); k += 1) {
1175
- if (pnOffset + k >= 0 && pnOffset + k < toI32(buf.length)) {
1176
- buf[pnOffset + k] = buf[pnOffset + k] ^ mask[k + 1]
1538
+ const masked: boolean = toI32(mask.length) >= 5
1539
+ if (masked) {
1540
+ buf[start] = toU8(first ^ (toI32(mask[0]) & quicPacketProtectedBits(first)))
1541
+ for (let k: i32 = 0; k < pnLength && k + 1 < toI32(mask.length); k += 1) {
1542
+ if (pnOffset + k >= 0 && pnOffset + k < toI32(buf.length)) {
1543
+ buf[pnOffset + k] = buf[pnOffset + k] ^ mask[k + 1]
1544
+ }
1177
1545
  }
1178
1546
  }
1547
+ wipe(mask)
1548
+ if (!masked) {
1549
+ return -1
1550
+ }
1179
1551
  }
1180
1552
  return end
1181
1553
  }
@@ -1263,13 +1635,16 @@ export const quicUnprotectHeader = (
1263
1635
  keys,
1264
1636
  quicPacketSlice(datagram, sampleAt, sampleAt + QUIC_SAMPLE_SIZE)
1265
1637
  )
1266
- if (mask === null || toI32(mask.length) < 5) {
1638
+ if (mask === null) {
1267
1639
  packet.error = QUIC_ERR_KEYS
1268
1640
  return []
1269
1641
  }
1642
+ const usable: boolean = toI32(mask.length) >= 5
1270
1643
  const protectedFirst: i32 = toI32(datagram[header.start])
1271
1644
  const long: boolean = (protectedFirst & 0x80) !== 0
1272
- const first: i32 = protectedFirst ^ (toI32(mask[0]) & quicPacketProtectedBits(protectedFirst))
1645
+ const first: i32 = usable
1646
+ ? protectedFirst ^ (toI32(mask[0]) & quicPacketProtectedBits(protectedFirst))
1647
+ : protectedFirst
1273
1648
  const pnLength: i32 = (first & 3) + 1
1274
1649
  // The sample check above put 20 bytes after `pnOffset` inside the packet,
1275
1650
  // so the packet number's at most 4 are there too.
@@ -1284,6 +1659,13 @@ export const quicUnprotectHeader = (
1284
1659
  truncated = (truncated << 8) | toI64(b)
1285
1660
  }
1286
1661
  }
1662
+ // The mask is the header-protection key's keystream: wiped once used, on
1663
+ // the one path every mask takes, so that `-O2` counts this wipe alone.
1664
+ wipe(mask)
1665
+ if (!usable) {
1666
+ packet.error = QUIC_ERR_KEYS
1667
+ return []
1668
+ }
1287
1669
  packet.firstByte = first
1288
1670
  packet.pnLength = pnLength
1289
1671
  packet.packetNumber = quicPacketNumberDecode(largestPn, truncated, pnLength)
@@ -1338,12 +1720,15 @@ export const quicDecryptPayload = (
1338
1720
  return null
1339
1721
  }
1340
1722
  const payloadStart: i32 = header.start + toI32(clear.length)
1723
+ // The nonce gives the IV away (see `quicSealInPlace`): wiped once used.
1724
+ const nonce: u8[] = quicPacketNonce(keys, packet.packetNumber)
1341
1725
  const plain: u8[] | null = quicPacketOpen(
1342
1726
  keys,
1343
- quicPacketNonce(keys, packet.packetNumber),
1727
+ nonce,
1344
1728
  clear,
1345
1729
  quicPacketSlice(datagram, payloadStart, header.end)
1346
1730
  )
1731
+ wipe(nonce)
1347
1732
  if (plain === null) {
1348
1733
  packet.error = QUIC_ERR_DECRYPT
1349
1734
  return null