-
-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
Fixes #19
- Loading branch information
There are no files selected for viewing
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 GitHub Actions / buntests/index.test.ts > AgeEncrypter > should encrypt (and decrypt) a file with a recipient
Check failure on line 45 in lib/x25519.ts GitHub Actions / buntests/index.test.ts > AgeEncrypter > should encrypt (and decrypt) a file with multiple recipients
Check failure on line 45 in lib/x25519.ts GitHub Actions / buntests/index.test.ts > AgeEncrypter > should encrypt (and decrypt) a file without Web Crypto
Check failure on line 45 in lib/x25519.ts GitHub Actions / test (18.x)tests/index.test.ts > AgeEncrypter > should encrypt (and decrypt) a file with a recipient
Check failure on line 45 in lib/x25519.ts GitHub Actions / test (18.x)tests/index.test.ts > AgeEncrypter > should encrypt (and decrypt) a file with multiple recipients
Check failure on line 45 in lib/x25519.ts GitHub Actions / test (18.x)tests/index.test.ts > AgeEncrypter > should encrypt (and decrypt) a file without Web Crypto
|
||
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"]) | ||
} |
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) |