age-encryption
Version:
<p align="center"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://github.com/FiloSottile/age/blob/main/logo/logo_white.svg"> <source media="(prefers-color-scheme: light)" srcset="https://github.com/FiloSottile/a
84 lines (83 loc) • 3.69 kB
JavaScript
import { x25519 } from "@noble/curves/ed25519.js";
const exportable = false;
let webCryptoOff = false;
export function forceWebCryptoOff(off) {
webCryptoOff = off;
}
export async function webCryptoFallback(func, fallback) {
if (webCryptoOff) {
return await fallback();
}
// We can't reliably detect X25519 support in WebCrypto in a performant way
// because Bun implemented importKey, but not deriveBits.
// https://github.com/oven-sh/bun/issues/20148
try {
return await func();
}
catch (error) {
if (error instanceof ReferenceError ||
error instanceof DOMException && error.name === "NotSupportedError") {
return await fallback();
}
else {
throw error;
}
}
}
export async function scalarMult(scalar, u) {
return await webCryptoFallback(async () => {
const key = isCryptoKey(scalar) ? scalar : await importX25519Key(scalar);
const peer = await crypto.subtle.importKey("raw", domBuffer(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));
}, () => {
if (isCryptoKey(scalar)) {
throw new Error("CryptoKey provided but X25519 WebCrypto is not supported");
}
return x25519.scalarMult(scalar, u);
});
}
export async function scalarMultBase(scalar) {
return await webCryptoFallback(async () => {
// 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);
}, () => {
if (isCryptoKey(scalar)) {
throw new Error("CryptoKey provided but X25519 WebCrypto is not supported");
}
return x25519.scalarMultBase(scalar);
});
}
const pkcs8Prefix = /* @__PURE__ */ new Uint8Array([
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06,
0x03, 0x2b, 0x65, 0x6e, 0x04, 0x22, 0x04, 0x20
]);
async function importX25519Key(key) {
// 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]);
// Annoyingly, 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"]);
}
function isCryptoKey(key) {
return typeof CryptoKey !== "undefined" && key instanceof CryptoKey;
}
// TypeScript 5.9+ made Uint8Array generic, defaulting to Uint8Array<ArrayBufferLike>.
// DOM APIs like crypto.subtle require Uint8Array<ArrayBuffer> (no SharedArrayBuffer).
// This helper narrows the type while still catching non-Uint8Array arguments.
function domBuffer(arr) {
return arr;
}