Skip to content

Commit

Permalink
Make main ops async, support CryptoKeys, and use Web Crypto if available
Browse files Browse the repository at this point in the history
Fixes #19
  • Loading branch information
FiloSottile committed Aug 11, 2024
1 parent 27d73ed commit 8ece90e
Show file tree
Hide file tree
Showing 12 changed files with 274 additions and 84 deletions.
22 changes: 12 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,18 @@ npm install age-encryption
```ts
import * as age from "age-encryption"

const identity = age.generateIdentity()
const recipient = age.identityToRecipient(identity)
const identity = await age.generateIdentity()
const recipient = await age.identityToRecipient(identity)
console.log(identity)
console.log(recipient)

const e = new age.Encrypter()
e.addRecipient(recipient)
const ciphertext = e.encrypt("Hello, age!")
const ciphertext = await e.encrypt("Hello, age!")

const d = new age.Decrypter()
d.addIdentity(identity)
const out = d.decrypt(ciphertext, "text")
const out = await d.decrypt(ciphertext, "text")
console.log(out)
```

Expand All @@ -49,11 +49,11 @@ import { Encrypter, Decrypter } from "age-encryption"

const e = new Encrypter()
e.setPassphrase("burst-swarm-slender-curve-ability-various-crystal-moon-affair-three")
const ciphertext = e.encrypt("Hello, age!")
const ciphertext = await e.encrypt("Hello, age!")

const d = new Decrypter()
d.addPassphrase("burst-swarm-slender-curve-ability-various-crystal-moon-affair-three")
const out = d.decrypt(ciphertext, "text")
const out = await d.decrypt(ciphertext, "text")
console.log(out)
```

Expand Down Expand Up @@ -81,18 +81,20 @@ Then, you can use it like this
```html
<script src="age.js"></script>
<script>
const identity = age.generateIdentity()
const recipient = age.identityToRecipient(identity)
(async () => {
const identity = await age.generateIdentity()
const recipient = await age.identityToRecipient(identity)
console.log(identity)
console.log(recipient)
const e = new age.Encrypter()
e.addRecipient(recipient)
const ciphertext = e.encrypt("Hello, age!")
const ciphertext = await e.encrypt("Hello, age!")
const d = new age.Decrypter()
d.addIdentity(identity)
const out = d.decrypt(ciphertext, "text")
const out = await d.decrypt(ciphertext, "text")
console.log(out)
})()
</script>
```
52 changes: 33 additions & 19 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,32 @@ import { bech32 } from "@scure/base"
import { hmac } from "@noble/hashes/hmac"
import { hkdf } from "@noble/hashes/hkdf"
import { sha256 } from "@noble/hashes/sha256"
import { x25519 } from "@noble/curves/ed25519"
import { randomBytes } from "@noble/hashes/utils"
import * as x25519 from "./x25519.js"
import { scryptUnwrap, scryptWrap, x25519Identity, x25519Unwrap, x25519Wrap } from "./recipients.js"
import { encodeHeader, encodeHeaderNoMAC, parseHeader, Stanza } from "./format.js"
import { decryptSTREAM, encryptSTREAM } from "./stream.js"

export function generateIdentity(): string {
export function generateIdentity(): Promise<string> {
const scalar = randomBytes(32)
return bech32.encode("AGE-SECRET-KEY-", bech32.toWords(scalar)).toUpperCase()
const identity = bech32.encode("AGE-SECRET-KEY-", bech32.toWords(scalar)).toUpperCase()
return Promise.resolve(identity)
}

export function identityToRecipient(identity: string): string {
const res = bech32.decodeToBytes(identity)
if (!identity.startsWith("AGE-SECRET-KEY-1") ||
res.prefix.toUpperCase() !== "AGE-SECRET-KEY-" ||
res.bytes.length !== 32)
throw Error("invalid identity")
export async function identityToRecipient(identity: string | CryptoKey): Promise<string> {
let scalar: Uint8Array | CryptoKey
if (identity instanceof CryptoKey) {

Check failure on line 19 in lib/index.ts

View workflow job for this annotation

GitHub Actions / bun

tests/index.test.ts > key generation > should encrypt and decrypt a file

ReferenceError: CryptoKey is not defined ❯ Module.identityToRecipient lib/index.ts:19:27 ❯ tests/index.test.ts:24:33

Check failure on line 19 in lib/index.ts

View workflow job for this annotation

GitHub Actions / bun

tests/property.test.ts > Property Based Tests > Asymmetric Encryption and Decryption > decryption should invert encryption with identity/recipient (string plaintext) (with seed=-191573507)

Error: Property failed after 1 tests { seed: -191573507, path: "0:0", endOnFailure: true } Counterexample: [{"plaintext":""}] Shrunk 1 time(s) Got ReferenceError: CryptoKey is not defined at Module.identityToRecipient (/home/runner/work/typage/typage/lib/index.ts:19:27) at /home/runner/work/typage/typage/tests/property.test.ts:35:33 at processTicksAndRejections (node:internal/process/task_queues:95:5) at AsyncProperty.run (file:///home/runner/work/typage/typage/node_modules/fast-check/lib/esm/check/property/AsyncProperty.generic.js:46:28) at asyncRunIt (file:///home/runner/work/typage/typage/node_modules/fast-check/lib/esm/check/runner/Runner.js:33:21) at file:///home/runner/work/typage/typage/node_modules/@fast-check/vitest/lib/internals/TestWithPropRunnerBuilder.js:22:9 at runTest (file:///home/runner/work/typage/typage/node_modules/@vitest/runner/dist/index.js:960:11) at runSuite (file:///home/runner/work/typage/typage/node_modules/@vitest/runner/dist/index.js:1116:15) at runSuite (file:///home/runner/work/typage/typage/node_modules/@vitest/runner/dist/index.js:1116:15) at runSuite (file:///home/runner/work/typage/typage/node_modules/@vitest/runner/dist/index.js:1116:15) Hint: Enable verbose mode in order to have the list of all failing values encountered during the run ❯ Module.identityToRecipient lib/index.ts:19:27 ❯ tests/property.test.ts:35:33 ❯ AsyncProperty.run node_modules/fast-check/lib/esm/check/property/AsyncProperty.generic.js:46:28 ❯ asyncRunIt node_modules/fast-check/lib/esm/check/runner/Runner.js:33:21 ❯ node_modules/@fast-check/vitest/lib/internals/TestWithPropRunnerBuilder.js:22:9 ❯ buildError node_modules/fast-check/lib/esm/check/runner/utils/RunDetailsFormatter.js:126:15 ❯ asyncThrowIfFailed node_modules/fast-check/lib/esm/check/runner/utils/RunDetailsFormatter.js:143:11

Check failure on line 19 in lib/index.ts

View workflow job for this annotation

GitHub Actions / bun

tests/property.test.ts > Property Based Tests > Asymmetric Encryption and Decryption > decryption should invert encryption with identity/recipient (uint8array plaintext) (with seed=1466880710)

Error: Property failed after 1 tests { seed: 1466880710, path: "0", endOnFailure: true } Counterexample: [{"plaintext":Uint8Array.from([])}] Shrunk 0 time(s) Got ReferenceError: CryptoKey is not defined at Module.identityToRecipient (/home/runner/work/typage/typage/lib/index.ts:19:27) at /home/runner/work/typage/typage/tests/property.test.ts:55:33 at processTicksAndRejections (node:internal/process/task_queues:95:5) at AsyncProperty.run (file:///home/runner/work/typage/typage/node_modules/fast-check/lib/esm/check/property/AsyncProperty.generic.js:46:28) at asyncRunIt (file:///home/runner/work/typage/typage/node_modules/fast-check/lib/esm/check/runner/Runner.js:33:21) at file:///home/runner/work/typage/typage/node_modules/@fast-check/vitest/lib/internals/TestWithPropRunnerBuilder.js:22:9 at runTest (file:///home/runner/work/typage/typage/node_modules/@vitest/runner/dist/index.js:960:11) at runSuite (file:///home/runner/work/typage/typage/node_modules/@vitest/runner/dist/index.js:1116:15) at runSuite (file:///home/runner/work/typage/typage/node_modules/@vitest/runner/dist/index.js:1116:15) at runSuite (file:///home/runner/work/typage/typage/node_modules/@vitest/runner/dist/index.js:1116:15) Hint: Enable verbose mode in order to have the list of all failing values encountered during the run ❯ Module.identityToRecipient lib/index.ts:19:27 ❯ tests/property.test.ts:55:33 ❯ AsyncProperty.run node_modules/fast-check/lib/esm/check/property/AsyncProperty.generic.js:46:28 ❯ asyncRunIt node_modules/fast-check/lib/esm/check/runner/Runner.js:33:21 ❯ node_modules/@fast-check/vitest/lib/internals/TestWithPropRunnerBuilder.js:22:9 ❯ buildError node_modules/fast-check/lib/esm/check/runner/utils/RunDetailsFormatter.js:126:15 ❯ asyncThrowIfFailed node_modules/fast-check/lib/esm/check/runner/utils/RunDetailsFormatter.js:143:11

Check failure on line 19 in lib/index.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

tests/index.test.ts > key generation > should encrypt and decrypt a file

ReferenceError: CryptoKey is not defined ❯ Module.identityToRecipient lib/index.ts:19:27 ❯ tests/index.test.ts:24:33

Check failure on line 19 in lib/index.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

tests/property.test.ts > Property Based Tests > Asymmetric Encryption and Decryption > decryption should invert encryption with identity/recipient (string plaintext) (with seed=-2020503881)

Error: Property failed after 1 tests { seed: -2020503881, path: "0:0", endOnFailure: true } Counterexample: [{"plaintext":""}] Shrunk 1 time(s) Got ReferenceError: CryptoKey is not defined at Module.identityToRecipient (/home/runner/work/typage/typage/lib/index.ts:19:27) at /home/runner/work/typage/typage/tests/property.test.ts:35:33 at processTicksAndRejections (node:internal/process/task_queues:95:5) at AsyncProperty.run (file:///home/runner/work/typage/typage/node_modules/fast-check/lib/esm/check/property/AsyncProperty.generic.js:46:28) at asyncRunIt (file:///home/runner/work/typage/typage/node_modules/fast-check/lib/esm/check/runner/Runner.js:33:21) at file:///home/runner/work/typage/typage/node_modules/@fast-check/vitest/lib/internals/TestWithPropRunnerBuilder.js:22:9 at runTest (file:///home/runner/work/typage/typage/node_modules/@vitest/runner/dist/index.js:960:11) at runSuite (file:///home/runner/work/typage/typage/node_modules/@vitest/runner/dist/index.js:1116:15) at runSuite (file:///home/runner/work/typage/typage/node_modules/@vitest/runner/dist/index.js:1116:15) at runSuite (file:///home/runner/work/typage/typage/node_modules/@vitest/runner/dist/index.js:1116:15) Hint: Enable verbose mode in order to have the list of all failing values encountered during the run ❯ Module.identityToRecipient lib/index.ts:19:27 ❯ tests/property.test.ts:35:33 ❯ AsyncProperty.run node_modules/fast-check/lib/esm/check/property/AsyncProperty.generic.js:46:28 ❯ asyncRunIt node_modules/fast-check/lib/esm/check/runner/Runner.js:33:21 ❯ node_modules/@fast-check/vitest/lib/internals/TestWithPropRunnerBuilder.js:22:9 ❯ buildError node_modules/fast-check/lib/esm/check/runner/utils/RunDetailsFormatter.js:126:15 ❯ asyncThrowIfFailed node_modules/fast-check/lib/esm/check/runner/utils/RunDetailsFormatter.js:143:11

Check failure on line 19 in lib/index.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

tests/property.test.ts > Property Based Tests > Asymmetric Encryption and Decryption > decryption should invert encryption with identity/recipient (uint8array plaintext) (with seed=159075802)

Error: Property failed after 1 tests { seed: 159075802, path: "0:0", endOnFailure: true } Counterexample: [{"plaintext":Uint8Array.from([])}] Shrunk 1 time(s) Got ReferenceError: CryptoKey is not defined at Module.identityToRecipient (/home/runner/work/typage/typage/lib/index.ts:19:27) at /home/runner/work/typage/typage/tests/property.test.ts:55:33 at processTicksAndRejections (node:internal/process/task_queues:95:5) at AsyncProperty.run (file:///home/runner/work/typage/typage/node_modules/fast-check/lib/esm/check/property/AsyncProperty.generic.js:46:28) at asyncRunIt (file:///home/runner/work/typage/typage/node_modules/fast-check/lib/esm/check/runner/Runner.js:33:21) at file:///home/runner/work/typage/typage/node_modules/@fast-check/vitest/lib/internals/TestWithPropRunnerBuilder.js:22:9 at runTest (file:///home/runner/work/typage/typage/node_modules/@vitest/runner/dist/index.js:960:11) at runSuite (file:///home/runner/work/typage/typage/node_modules/@vitest/runner/dist/index.js:1116:15) at runSuite (file:///home/runner/work/typage/typage/node_modules/@vitest/runner/dist/index.js:1116:15) at runSuite (file:///home/runner/work/typage/typage/node_modules/@vitest/runner/dist/index.js:1116:15) Hint: Enable verbose mode in order to have the list of all failing values encountered during the run ❯ Module.identityToRecipient lib/index.ts:19:27 ❯ tests/property.test.ts:55:33 ❯ AsyncProperty.run node_modules/fast-check/lib/esm/check/property/AsyncProperty.generic.js:46:28 ❯ asyncRunIt node_modules/fast-check/lib/esm/check/runner/Runner.js:33:21 ❯ node_modules/@fast-check/vitest/lib/internals/TestWithPropRunnerBuilder.js:22:9 ❯ buildError node_modules/fast-check/lib/esm/check/runner/utils/RunDetailsFormatter.js:126:15 ❯ asyncThrowIfFailed node_modules/fast-check/lib/esm/check/runner/utils/RunDetailsFormatter.js:143:11
scalar = identity
} else {
const res = bech32.decodeToBytes(identity)
if (!identity.startsWith("AGE-SECRET-KEY-1") ||
res.prefix.toUpperCase() !== "AGE-SECRET-KEY-" ||
res.bytes.length !== 32)
throw Error("invalid identity")
scalar = res.bytes
}

const recipient = x25519.scalarMultBase(res.bytes)
const recipient = await x25519.scalarMultBase(scalar)
return bech32.encode("age", bech32.toWords(recipient))
}

Expand Down Expand Up @@ -52,7 +59,7 @@ export class Encrypter {
this.recipients.push(res.bytes)
}

encrypt(file: Uint8Array | string): Uint8Array {
async encrypt(file: Uint8Array | string): Promise<Uint8Array> {
if (typeof file === "string") {
file = new TextEncoder().encode(file)
}
Expand All @@ -61,7 +68,7 @@ export class Encrypter {
const stanzas: Stanza[] = []

for (const recipient of this.recipients) {
stanzas.push(x25519Wrap(fileKey, recipient))
stanzas.push(await x25519Wrap(fileKey, recipient))
}
if (this.passphrase !== null) {
stanzas.push(scryptWrap(fileKey, this.passphrase, this.scryptWorkFactor))
Expand Down Expand Up @@ -91,7 +98,14 @@ export class Decrypter {
this.passphrases.push(s)
}

addIdentity(s: string): void {
addIdentity(s: string | CryptoKey): void {
if (s instanceof CryptoKey) {

Check failure on line 102 in lib/index.ts

View workflow job for this annotation

GitHub Actions / bun

tests/index.test.ts > AgeDecrypter > should decrypt a file with the right identity

ReferenceError: CryptoKey is not defined ❯ Decrypter.addIdentity lib/index.ts:102:22 ❯ tests/index.test.ts:15:11

Check failure on line 102 in lib/index.ts

View workflow job for this annotation

GitHub Actions / bun

tests/testkit.test.ts > CCTV testkit > header_crlf should fail

ReferenceError: CryptoKey is not defined ❯ Decrypter.addIdentity lib/index.ts:102:22 ❯ tests/testkit.test.ts:96:23

Check failure on line 102 in lib/index.ts

View workflow job for this annotation

GitHub Actions / bun

tests/testkit.test.ts > CCTV testkit > header_crlf should fail without Web Crypto

ReferenceError: CryptoKey is not defined ❯ Decrypter.addIdentity lib/index.ts:102:22 ❯ tests/testkit.test.ts:103:23

Check failure on line 102 in lib/index.ts

View workflow job for this annotation

GitHub Actions / bun

tests/testkit.test.ts > CCTV testkit > hmac_bad should fail

ReferenceError: CryptoKey is not defined ❯ Decrypter.addIdentity lib/index.ts:102:22 ❯ tests/testkit.test.ts:96:23

Check failure on line 102 in lib/index.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

tests/index.test.ts > AgeDecrypter > should decrypt a file with the right identity

ReferenceError: CryptoKey is not defined ❯ Decrypter.addIdentity lib/index.ts:102:22 ❯ tests/index.test.ts:15:11

Check failure on line 102 in lib/index.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

tests/testkit.test.ts > CCTV testkit > header_crlf should fail

ReferenceError: CryptoKey is not defined ❯ Decrypter.addIdentity lib/index.ts:102:22 ❯ tests/testkit.test.ts:96:23

Check failure on line 102 in lib/index.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

tests/testkit.test.ts > CCTV testkit > header_crlf should fail without Web Crypto

ReferenceError: CryptoKey is not defined ❯ Decrypter.addIdentity lib/index.ts:102:22 ❯ tests/testkit.test.ts:103:23

Check failure on line 102 in lib/index.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

tests/testkit.test.ts > CCTV testkit > hmac_bad should fail

ReferenceError: CryptoKey is not defined ❯ Decrypter.addIdentity lib/index.ts:102:22 ❯ tests/testkit.test.ts:96:23
this.identities.push({
identity: s,
recipient: x25519.scalarMultBase(s),
})
return
}
const res = bech32.decodeToBytes(s)
if (!s.startsWith("AGE-SECRET-KEY-1") ||
res.prefix.toUpperCase() !== "AGE-SECRET-KEY-" ||
Expand All @@ -103,11 +117,11 @@ export class Decrypter {
})
}

decrypt(file: Uint8Array, outputFormat?: "uint8array"): Uint8Array
decrypt(file: Uint8Array, outputFormat: "text"): string
decrypt(file: Uint8Array, outputFormat?: "text" | "uint8array"): Uint8Array | string {
async decrypt(file: Uint8Array, outputFormat?: "uint8array"): Promise<Uint8Array>
async decrypt(file: Uint8Array, outputFormat: "text"): Promise<string>
async decrypt(file: Uint8Array, outputFormat?: "text" | "uint8array"): Promise<string | Uint8Array> {
const h = parseHeader(file)
const fileKey = this.unwrapFileKey(h.recipients)
const fileKey = await this.unwrapFileKey(h.recipients)
if (fileKey === null) {
throw Error("no identity matched any of the file's recipients")
}
Expand All @@ -127,7 +141,7 @@ export class Decrypter {
return out
}

private unwrapFileKey(recipients: Stanza[]): Uint8Array | null {
private async unwrapFileKey(recipients: Stanza[]): Promise<Uint8Array | null> {
for (const s of recipients) {
// Ideally this should be implemented by passing all stanzas to the scrypt
// identity implementation, and letting it throw the error. In practice,
Expand All @@ -142,7 +156,7 @@ export class Decrypter {
}

for (const i of this.identities) {
const k = x25519Unwrap(s, i)
const k = await x25519Unwrap(s, i)
if (k !== null) { return k }
}
}
Expand Down
19 changes: 10 additions & 9 deletions lib/recipients.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { hkdf } from "@noble/hashes/hkdf"
import { sha256 } from "@noble/hashes/sha256"
import { scrypt } from "@noble/hashes/scrypt"
import { x25519 } from "@noble/curves/ed25519"
import { chacha20poly1305 } from "@noble/ciphers/chacha"
import { randomBytes } from "@noble/hashes/utils"
import { base64nopad } from "@scure/base"
import * as x25519 from "./x25519.js"
import { Stanza } from "./format.js"

export interface x25519Identity {
identity: Uint8Array, recipient: Uint8Array,
identity: Uint8Array | CryptoKey, recipient: Promise<Uint8Array>,
}

export function x25519Wrap(fileKey: Uint8Array, recipient: Uint8Array): Stanza {
export async function x25519Wrap(fileKey: Uint8Array, recipient: Uint8Array): Promise<Stanza> {
const ephemeral = randomBytes(32)
const share = x25519.scalarMultBase(ephemeral)
const secret = x25519.scalarMult(ephemeral, recipient)
const share = await x25519.scalarMultBase(ephemeral)
const secret = await x25519.scalarMult(ephemeral, recipient)

const salt = new Uint8Array(share.length + recipient.length)
salt.set(share)
Expand All @@ -24,7 +24,7 @@ export function x25519Wrap(fileKey: Uint8Array, recipient: Uint8Array): Stanza {
return new Stanza(["X25519", base64nopad.encode(share)], encryptFileKey(fileKey, key))
}

export function x25519Unwrap(s: Stanza, i: x25519Identity): Uint8Array | null {
export async function x25519Unwrap(s: Stanza, i: x25519Identity): Promise<Uint8Array | null> {
if (s.args.length < 1 || s.args[0] !== "X25519") {
return null
}
Expand All @@ -36,11 +36,12 @@ export function x25519Unwrap(s: Stanza, i: x25519Identity): Uint8Array | null {
throw Error("invalid X25519 stanza")
}

const secret = x25519.scalarMult(i.identity, share)
const secret = await x25519.scalarMult(i.identity, share)

const salt = new Uint8Array(share.length + i.recipient.length)
const recipient = await i.recipient
const salt = new Uint8Array(share.length + recipient.length)
salt.set(share)
salt.set(i.recipient, share.length)
salt.set(recipient, share.length)

const key = hkdf(sha256, secret, salt, "age-encryption.org/v1/X25519", 32)
return decryptFileKey(s.body, key)
Expand Down
75 changes: 75 additions & 0 deletions lib/x25519.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { x25519 } from "@noble/curves/ed25519"

const exportable = false

let webCryptoOff = false

export function forceWebCryptoOff(off: boolean) {
webCryptoOff = off
}

export const isX25519Supported = (() => {
let supported: boolean | undefined
return async () => {
if (supported === undefined) {
try {
await crypto.subtle.importKey("raw", x25519.GuBytes, { name: "X25519" }, exportable, [])
supported = true
} catch { supported = false }
}
return supported
}
})()

export async function scalarMult(scalar: Uint8Array | CryptoKey, u: Uint8Array): Promise<Uint8Array> {
if (!(await isX25519Supported()) || webCryptoOff) {
if (scalar instanceof CryptoKey) {
throw new Error("CryptoKey provided but X25519 WebCrypto is not supported")
}
return x25519.scalarMult(scalar, u)
}
let key: CryptoKey
if (scalar instanceof CryptoKey) {
key = scalar
} else {
key = await importX25519Key(scalar)
}
const peer = await crypto.subtle.importKey("raw", u, { name: "X25519" }, exportable, [])
// 256 bits is the fixed size of a X25519 shared secret. It's kind of
// worrying that the WebCrypto API encourages truncating it.
return new Uint8Array(await crypto.subtle.deriveBits({ name: "X25519", public: peer }, key, 256))
}

export async function scalarMultBase(scalar: Uint8Array | CryptoKey): Promise<Uint8Array> {
if (!(await isX25519Supported()) || webCryptoOff) {
if (scalar instanceof CryptoKey) {

Check failure on line 45 in lib/x25519.ts

View workflow job for this annotation

GitHub Actions / bun

tests/index.test.ts > AgeEncrypter > should encrypt (and decrypt) a file with a recipient

ReferenceError: CryptoKey is not defined ❯ Module.scalarMultBase lib/x25519.ts:45:31 ❯ Module.x25519Wrap lib/recipients.ts:16:19 ❯ Encrypter.encrypt lib/index.ts:71:20 ❯ tests/index.test.ts:54:22

Check failure on line 45 in lib/x25519.ts

View workflow job for this annotation

GitHub Actions / bun

tests/index.test.ts > AgeEncrypter > should encrypt (and decrypt) a file with multiple recipients

ReferenceError: CryptoKey is not defined ❯ Module.scalarMultBase lib/x25519.ts:45:31 ❯ Module.x25519Wrap lib/recipients.ts:16:19 ❯ Encrypter.encrypt lib/index.ts:71:20 ❯ tests/index.test.ts:66:22

Check failure on line 45 in lib/x25519.ts

View workflow job for this annotation

GitHub Actions / bun

tests/index.test.ts > AgeEncrypter > should encrypt (and decrypt) a file without Web Crypto

ReferenceError: CryptoKey is not defined ❯ Module.scalarMultBase lib/x25519.ts:45:31 ❯ Module.x25519Wrap lib/recipients.ts:16:19 ❯ Encrypter.encrypt lib/index.ts:71:20 ❯ tests/index.test.ts:100:22

Check failure on line 45 in lib/x25519.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

tests/index.test.ts > AgeEncrypter > should encrypt (and decrypt) a file with a recipient

ReferenceError: CryptoKey is not defined ❯ Module.scalarMultBase lib/x25519.ts:45:31 ❯ Module.x25519Wrap lib/recipients.ts:16:19 ❯ Encrypter.encrypt lib/index.ts:71:20 ❯ tests/index.test.ts:54:22

Check failure on line 45 in lib/x25519.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

tests/index.test.ts > AgeEncrypter > should encrypt (and decrypt) a file with multiple recipients

ReferenceError: CryptoKey is not defined ❯ Module.scalarMultBase lib/x25519.ts:45:31 ❯ Module.x25519Wrap lib/recipients.ts:16:19 ❯ Encrypter.encrypt lib/index.ts:71:20 ❯ tests/index.test.ts:66:22

Check failure on line 45 in lib/x25519.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

tests/index.test.ts > AgeEncrypter > should encrypt (and decrypt) a file without Web Crypto

ReferenceError: CryptoKey is not defined ❯ Module.scalarMultBase lib/x25519.ts:45:31 ❯ Module.x25519Wrap lib/recipients.ts:16:19 ❯ Encrypter.encrypt lib/index.ts:71:20 ❯ tests/index.test.ts:100:22
throw new Error("CryptoKey provided but X25519 WebCrypto is not supported")
}
return x25519.scalarMultBase(scalar)
}
// The WebCrypto API simply doesn't support deriving public keys from
// private keys. importKey returns only a CryptoKey (unlike generateKey
// which returns a CryptoKeyPair) despite deriving the public key internally
// (judging from the banchmarks, at least on Node.js). Our options are
// exporting as JWK, deleting jwk.d, and re-importing (which only works for
// exportable keys), or (re-)doing a scalar multiplication by the basepoint
// manually. Here we do the latter.
return scalarMult(scalar, x25519.GuBytes)
}

const pkcs8Prefix = new Uint8Array([0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06,
0x03, 0x2b, 0x65, 0x6e, 0x04, 0x22, 0x04, 0x20])

async function importX25519Key(key: Uint8Array): Promise<CryptoKey> {
// For some reason, the WebCrypto API only supports importing X25519 private
// keys as PKCS #8 or JWK (even if it supports importing public keys as raw).
// Thankfully since they are always the same length, we can just prepend a
// fixed ASN.1 prefix for PKCS #8.
if (key.length !== 32) {
throw new Error("X25519 private key must be 32 bytes")
}
const pkcs8 = new Uint8Array([...pkcs8Prefix, ...key])
// Annoingly, importKey (at least on Node.js) computes the public key, which
// is a waste if we're only going to run deriveBits.
return crypto.subtle.importKey("pkcs8", pkcs8, { name: "X25519" }, exportable, ["deriveBits"])
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"examples:node": "cd tests/examples && npm install && npm run test:node",
"examples:bun": "cd tests/examples && bun install && bun run test:bun",
"examples:esbuild": "cd tests/examples && npm install && npm run test:esbuild",
"bench": "vitest bench --run",
"lint": "eslint .",
"build": "tsc -p tsconfig.build.json",
"prepublishOnly": "npm run build"
Expand Down
10 changes: 6 additions & 4 deletions tests/examples/browser.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@
<head>
<script src="age.js"></script>
<script>
const identity = age.generateIdentity()
const recipient = age.identityToRecipient(identity)
(async () => {
const identity = await age.generateIdentity()
const recipient = await age.identityToRecipient(identity)
console.log(identity)
console.log(recipient)

const e = new age.Encrypter()
e.addRecipient(recipient)
const ciphertext = e.encrypt("Hello, age!")
const ciphertext = await e.encrypt("Hello, age!")

const d = new age.Decrypter()
d.addIdentity(identity)
const out = d.decrypt(ciphertext, "text")
const out = await d.decrypt(ciphertext, "text")
console.log(out)

globalThis.testDone = true
})()
</script>
8 changes: 4 additions & 4 deletions tests/examples/identity.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Encrypter, Decrypter, generateIdentity, identityToRecipient } from "age-encryption"

const identity = generateIdentity()
const recipient = identityToRecipient(identity)
const identity = await generateIdentity()
const recipient = await identityToRecipient(identity)
console.log(identity)
console.log(recipient)

const e = new Encrypter()
e.addRecipient(recipient)
const ciphertext = e.encrypt("Hello, age!")
const ciphertext = await e.encrypt("Hello, age!")

const d = new Decrypter()
d.addIdentity(identity)
const out = d.decrypt(ciphertext, "text")
const out = await d.decrypt(ciphertext, "text")
console.log(out)
4 changes: 2 additions & 2 deletions tests/examples/passphrase.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { Encrypter, Decrypter } from "age-encryption"
const e = new Encrypter()
e.setScryptWorkFactor(12) // this is NOT secure, used to avoid extra work in tests
e.setPassphrase("burst-swarm-slender-curve-ability-various-crystal-moon-affair-three")
const ciphertext = e.encrypt("Hello, age!")
const ciphertext = await e.encrypt("Hello, age!")

const d = new Decrypter()
d.addPassphrase("burst-swarm-slender-curve-ability-various-crystal-moon-affair-three")
const out = d.decrypt(ciphertext, "text")
const out = await d.decrypt(ciphertext, "text")
console.log(out)
Loading

0 comments on commit 8ece90e

Please sign in to comment.