Skip to content

Commit 7218c6c

Browse files
committed
Update dependencies and CI
1 parent aaec61a commit 7218c6c

File tree

15 files changed

+1545
-1235
lines changed

15 files changed

+1545
-1235
lines changed

.github/workflows/build.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
persist-credentials: false
1717
- uses: actions/setup-node@v4
1818
with:
19-
node-version: 24.x # Current as of 2025-07
19+
node-version: 25.x # Current as of 2025-12
2020
- run: npm clean-install
2121
- run: npm run build
2222
- run: npm run lint
@@ -29,7 +29,7 @@ jobs:
2929
persist-credentials: false
3030
- uses: actions/setup-node@v4
3131
with:
32-
node-version: 24.x
32+
node-version: 25.x # Current as of 2025-12
3333
- run: npm clean-install
3434
- run: npm run build
3535
- run: node_modules/.bin/esbuild --target=es2022 --bundle --outfile=typage.js --global-name=age age-encryption
@@ -74,7 +74,7 @@ jobs:
7474
persist-credentials: false
7575
- uses: actions/setup-node@v4
7676
with:
77-
node-version: 24.x
77+
node-version: 25.x # Current as of 2025-12
7878
registry-url: "https://registry.npmjs.org"
7979
- run: npm clean-install
8080
- run: npx jsr publish

.github/workflows/test.yml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ jobs:
1212
strategy:
1313
matrix:
1414
node-version:
15-
- 20.x # Maintenance LTS as of 2025-07
16-
- 22.x # Active LTS as of 2025-07
17-
- 24.x # Current as of 2025-07
15+
- 20.x # Maintenance LTS as of 2025-12
16+
- 22.x # Maintenance LTS as of 2025-12
17+
- 24.x # Active LTS as of 2025-12
18+
- 25.x # Current as of 2025-12
1819
steps:
1920
- uses: actions/checkout@v4
2021
with:
@@ -36,7 +37,7 @@ jobs:
3637
persist-credentials: false
3738
- uses: actions/setup-node@v4
3839
with:
39-
node-version: 24.x
40+
node-version: 25.x # Current as of 2025-12
4041
- run: npm update
4142
- run: npm run build
4243
- run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
@@ -49,7 +50,7 @@ jobs:
4950
persist-credentials: false
5051
- uses: actions/setup-node@v4
5152
with:
52-
node-version: 24.x
53+
node-version: 25.x # Current as of 2025-12
5354
- uses: oven-sh/setup-bun@v1
5455
with:
5556
bun-version: latest
@@ -66,7 +67,7 @@ jobs:
6667
persist-credentials: false
6768
- uses: actions/setup-node@v4
6869
with:
69-
node-version: 24.x
70+
node-version: 25.x # Current as of 2025-12
7071
- uses: denoland/setup-deno@v2
7172
with:
7273
deno-version: vx.x.x
@@ -81,7 +82,7 @@ jobs:
8182
persist-credentials: false
8283
- uses: actions/setup-node@v4
8384
with:
84-
node-version: 24.x
85+
node-version: 25.x # Current as of 2025-12
8586
- run: npm update
8687
- run: npm run build
8788
- run: npm run examples:yarn
@@ -93,7 +94,7 @@ jobs:
9394
persist-credentials: false
9495
- uses: actions/setup-node@v4
9596
with:
96-
node-version: 24.x
97+
node-version: 25.x # Current as of 2025-12
9798
- uses: pnpm/action-setup@v4
9899
with:
99100
version: latest

eslint.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import eslint from "@eslint/js"
2+
import { defineConfig } from "eslint/config"
23
import tseslint from "typescript-eslint"
34
import stylistic from "@stylistic/eslint-plugin"
45
import tsdoc from "eslint-plugin-tsdoc"
56

6-
export default tseslint.config(
7+
export default defineConfig(
78
// Global ignores for generated files.
89
{ ignores: ["dist/", "tests/examples/age.js"] },
910

lib/index.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { hmac } from "@noble/hashes/hmac"
2-
import { hkdf } from "@noble/hashes/hkdf"
3-
import { sha256 } from "@noble/hashes/sha2"
4-
import { randomBytes } from "@noble/hashes/utils"
1+
import { hmac } from "@noble/hashes/hmac.js"
2+
import { hkdf } from "@noble/hashes/hkdf.js"
3+
import { sha256 } from "@noble/hashes/sha2.js"
4+
import { randomBytes } from "@noble/hashes/utils.js"
55
import { HybridIdentity, HybridRecipient, ScryptIdentity, ScryptRecipient, X25519Identity, X25519Recipient } from "./recipients.js"
66
import { encodeHeader, encodeHeaderNoMAC, parseHeader, Stanza } from "./format.js"
77
import { ciphertextSize, decryptSTREAM, encryptSTREAM, plaintextSize } from "./stream.js"
@@ -183,12 +183,14 @@ export class Encrypter {
183183
stanzas.push(...await recipient.wrapFileKey(fileKey))
184184
}
185185

186-
const hmacKey = hkdf(sha256, fileKey, undefined, "header", 32)
186+
const labelHeader = new TextEncoder().encode("header")
187+
const hmacKey = hkdf(sha256, fileKey, undefined, labelHeader, 32)
187188
const mac = hmac(sha256, hmacKey, encodeHeaderNoMAC(stanzas))
188189
const header = encodeHeader(stanzas, mac)
189190

190191
const nonce = randomBytes(16)
191-
const streamKey = hkdf(sha256, fileKey, nonce, "payload", 32)
192+
const labelPayload = new TextEncoder().encode("payload")
193+
const streamKey = hkdf(sha256, fileKey, nonce, labelPayload, 32)
192194
const encrypter = encryptSTREAM(streamKey)
193195

194196
if (!(file instanceof ReadableStream)) {
@@ -282,7 +284,8 @@ export class Decrypter {
282284
const { fileKey, headerSize, rest } = await this.decryptHeaderInternal(s)
283285
const { data: nonce, rest: payload } = await read(rest, 16)
284286

285-
const streamKey = hkdf(sha256, fileKey, nonce, "payload", 32)
287+
const label = new TextEncoder().encode("payload")
288+
const streamKey = hkdf(sha256, fileKey, nonce, label, 32)
286289
const decrypter = decryptSTREAM(streamKey)
287290
const out = payload.pipeThrough(decrypter)
288291

@@ -316,7 +319,8 @@ export class Decrypter {
316319
const fileKey = await this.unwrapFileKey(h.stanzas)
317320
if (fileKey === null) throw Error("no identity matched any of the file's recipients")
318321

319-
const hmacKey = hkdf(sha256, fileKey, undefined, "header", 32)
322+
const label = new TextEncoder().encode("header")
323+
const hmacKey = hkdf(sha256, fileKey, undefined, label, 32)
320324
const mac = hmac(sha256, hmacKey, h.headerNoMAC)
321325
if (!compareBytes(h.MAC, mac)) throw Error("invalid header HMAC")
322326

lib/io.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { randomBytes } from "@noble/hashes/utils"
1+
import { randomBytes } from "@noble/hashes/utils.js"
22

33
export class LineReader {
44
private s: ReadableStreamDefaultReader<Uint8Array>

lib/recipients.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { bech32 } from "@scure/base"
2-
import { hkdf, extract, expand } from "@noble/hashes/hkdf"
3-
import { sha256 } from "@noble/hashes/sha2"
4-
import { scrypt } from "@noble/hashes/scrypt"
5-
import { chacha20poly1305 } from "@noble/ciphers/chacha"
6-
import { XWing } from "@noble/post-quantum/hybrid.js"
7-
import { randomBytes } from "@noble/hashes/utils"
2+
import { hkdf, extract, expand } from "@noble/hashes/hkdf.js"
3+
import { sha256 } from "@noble/hashes/sha2.js"
4+
import { scrypt } from "@noble/hashes/scrypt.js"
5+
import { chacha20poly1305 } from "@noble/ciphers/chacha.js"
6+
import { MLKEM768X25519 } from "@noble/post-quantum/hybrid.js"
7+
import { randomBytes } from "@noble/hashes/utils.js"
88
import { base64nopad } from "@scure/base"
99
import * as x25519 from "./x25519.js"
1010
import { Stanza } from "./format.js"
@@ -73,7 +73,7 @@ export async function identityToRecipient(identity: string | CryptoKey): Promise
7373
const res = bech32.decodeToBytes(identity)
7474
if (res.prefix.toUpperCase() !== "AGE-SECRET-KEY-PQ-" ||
7575
res.bytes.length !== 32) { throw Error("invalid identity") }
76-
const recipient = XWing.getPublicKey(res.bytes)
76+
const recipient = MLKEM768X25519.getPublicKey(res.bytes)
7777
// Use encode directly to disable the 90 character bech32 limit.
7878
return bech32.encode("age1pq", bech32.toWords(recipient), false)
7979
} else {
@@ -99,7 +99,7 @@ export class HybridRecipient implements Recipient {
9999
}
100100

101101
wrapFileKey(fileKey: Uint8Array): Stanza[] {
102-
const { cipherText: encapsulatedKey, sharedSecret } = XWing.encapsulate(this.recipient)
102+
const { cipherText: encapsulatedKey, sharedSecret } = MLKEM768X25519.encapsulate(this.recipient)
103103
const label = new TextEncoder().encode("age-encryption.org/mlkem768x25519")
104104
const { key, nonce } = hpkeContext(hpkeMLKEM768X25519, sharedSecret, label)
105105
const ciphertext = chacha20poly1305(key, nonce).encrypt(fileKey)
@@ -134,7 +134,7 @@ export class HybridIdentity implements Identity {
134134
throw Error("invalid mlkem768x25519 stanza")
135135
}
136136

137-
const sharedSecret = XWing.decapsulate(share, this.identity)
137+
const sharedSecret = MLKEM768X25519.decapsulate(share, this.identity)
138138
const label = new TextEncoder().encode("age-encryption.org/mlkem768x25519")
139139
const { key, nonce } = hpkeContext(hpkeMLKEM768X25519, sharedSecret, label)
140140
try {
@@ -226,7 +226,8 @@ export class X25519Recipient implements Recipient {
226226
salt.set(share)
227227
salt.set(this.recipient, share.length)
228228

229-
const key = hkdf(sha256, secret, salt, "age-encryption.org/v1/X25519", 32)
229+
const label = new TextEncoder().encode("age-encryption.org/v1/X25519")
230+
const key = hkdf(sha256, secret, salt, label, 32)
230231
return [new Stanza(["X25519", base64nopad.encode(share)], encryptFileKey(fileKey, key))]
231232
}
232233
}
@@ -269,7 +270,8 @@ export class X25519Identity implements Identity {
269270
salt.set(share)
270271
salt.set(recipient, share.length)
271272

272-
const key = hkdf(sha256, secret, salt, "age-encryption.org/v1/X25519", 32)
273+
const label = new TextEncoder().encode("age-encryption.org/v1/X25519")
274+
const key = hkdf(sha256, secret, salt, label, 32)
273275
const fileKey = decryptFileKey(s.body, key)
274276
if (fileKey !== null) return fileKey
275277
}

lib/stream.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { chacha20poly1305 } from "@noble/ciphers/chacha"
1+
import { chacha20poly1305 } from "@noble/ciphers/chacha.js"
22

33
const chacha20poly1305Overhead = 16
44

lib/webauthn.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { bech32, base64nopad } from "@scure/base"
2-
import { randomBytes } from "@noble/hashes/utils"
3-
import { extract } from "@noble/hashes/hkdf"
4-
import { sha256 } from "@noble/hashes/sha2"
2+
import { randomBytes } from "@noble/hashes/utils.js"
3+
import { extract } from "@noble/hashes/hkdf.js"
4+
import { sha256 } from "@noble/hashes/sha2.js"
55
import { type Identity, type Recipient } from "./index.js"
66
import { Stanza } from "./format.js"
77
import { decryptFileKey, encryptFileKey } from "./recipients.js"
@@ -94,7 +94,7 @@ export async function createCredential(options: CreationOptions): Promise<string
9494
rp: { name: "", id: options.rpId },
9595
user: {
9696
name: options.keyName,
97-
id: randomBytes(8), // avoid overwriting existing keys
97+
id: domBuffer(randomBytes(8)), // avoid overwriting existing keys
9898
displayName: "",
9999
},
100100
pubKeyCredParams: defaultAlgorithms,
@@ -192,11 +192,11 @@ class WebAuthnInternal {
192192
const assertion = await navigator.credentials.get({
193193
publicKey: {
194194
allowCredentials: this.credId ? [{
195-
id: this.credId,
195+
id: domBuffer(this.credId),
196196
transports: this.transports as AuthenticatorTransport[],
197197
type: "public-key"
198198
}] : [],
199-
challenge: randomBytes(16),
199+
challenge: domBuffer(randomBytes(16)),
200200
extensions: { prf: { eval: prfInputs(nonce) } },
201201
userVerification: "required", // prf requires UV
202202
rpId: this.rpId,
@@ -298,5 +298,12 @@ function deriveKey(results: AuthenticationExtensionsPRFValues): Uint8Array {
298298
const prf = new Uint8Array(results.first.byteLength + results.second.byteLength)
299299
prf.set(new Uint8Array(results.first as ArrayBuffer), 0)
300300
prf.set(new Uint8Array(results.second as ArrayBuffer), results.first.byteLength)
301-
return extract(sha256, prf, label)
301+
return extract(sha256, prf, new TextEncoder().encode(label))
302+
}
303+
304+
// TypeScript 5.9+ made Uint8Array generic, defaulting to Uint8Array<ArrayBufferLike>.
305+
// DOM APIs like WebAuthn require Uint8Array<ArrayBuffer> (no SharedArrayBuffer).
306+
// This helper narrows the type while still catching non-Uint8Array arguments.
307+
function domBuffer(arr: Uint8Array): Uint8Array<ArrayBuffer> {
308+
return arr as Uint8Array<ArrayBuffer>
302309
}

lib/x25519.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { x25519 } from "@noble/curves/ed25519"
1+
import { x25519 } from "@noble/curves/ed25519.js"
22

33
const exportable = false
44

@@ -33,7 +33,7 @@ export async function webCryptoFallback<Return>(
3333
export async function scalarMult(scalar: Uint8Array | CryptoKey, u: Uint8Array): Promise<Uint8Array> {
3434
return await webCryptoFallback(async () => {
3535
const key = isCryptoKey(scalar) ? scalar : await importX25519Key(scalar)
36-
const peer = await crypto.subtle.importKey("raw", u, { name: "X25519" }, exportable, [])
36+
const peer = await crypto.subtle.importKey("raw", domBuffer(u), { name: "X25519" }, exportable, [])
3737
// 256 bits is the fixed size of a X25519 shared secret. It's kind of
3838
// worrying that the WebCrypto API encourages truncating it.
3939
return new Uint8Array(await crypto.subtle.deriveBits({ name: "X25519", public: peer }, key, 256))
@@ -84,3 +84,10 @@ async function importX25519Key(key: Uint8Array): Promise<CryptoKey> {
8484
function isCryptoKey(key: unknown): key is CryptoKey {
8585
return typeof CryptoKey !== "undefined" && key instanceof CryptoKey
8686
}
87+
88+
// TypeScript 5.9+ made Uint8Array generic, defaulting to Uint8Array<ArrayBufferLike>.
89+
// DOM APIs like crypto.subtle require Uint8Array<ArrayBuffer> (no SharedArrayBuffer).
90+
// This helper narrows the type while still catching non-Uint8Array arguments.
91+
function domBuffer(arr: Uint8Array): Uint8Array<ArrayBuffer> {
92+
return arr as Uint8Array<ArrayBuffer>
93+
}

0 commit comments

Comments
 (0)