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
77 lines (76 loc) • 3.17 kB
JavaScript
import { x25519 } from "@noble/curves/ed25519";
const exportable = false;
let webCryptoOff = false;
export function forceWebCryptoOff(off) {
webCryptoOff = off;
}
export const isX25519Supported = (() => {
let supported;
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, u) {
if (!(await isX25519Supported()) || webCryptoOff) {
if (isCryptoKey(scalar)) {
throw new Error("CryptoKey provided but X25519 WebCrypto is not supported");
}
return x25519.scalarMult(scalar, u);
}
let key;
if (isCryptoKey(scalar)) {
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) {
if (!(await isX25519Supported()) || webCryptoOff) {
if (isCryptoKey(scalar)) {
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 = /* @__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]);
// 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"]);
}
function isCryptoKey(key) {
return typeof CryptoKey !== "undefined" && key instanceof CryptoKey;
}