From 6106c68a03919bc1db5f8c5b78a8b51d87f64661 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Tue, 21 Jan 2025 01:19:46 +0800 Subject: [PATCH] feat: add deno-wrapkey-mod.ts --- libraries/deno-wrapkey-mod.ts | 139 ++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 libraries/deno-wrapkey-mod.ts diff --git a/libraries/deno-wrapkey-mod.ts b/libraries/deno-wrapkey-mod.ts new file mode 100644 index 0000000..2fbf13b --- /dev/null +++ b/libraries/deno-wrapkey-mod.ts @@ -0,0 +1,139 @@ +import { crypto } from "jsr:@std/crypto"; +import { encodeBase64Url } from "jsr:@std/encoding/base64url"; +import { decodeHex } from "jsr:@std/encoding/hex"; + +const { + createECDH, + getRandomValues, +} = await import("node:crypto"); + +// Reference: +// - https://docs.deno.com/api/node/crypto/~/ECDH +// - https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt + +class EcdhEphemeral { + ePublicKey: Uint8Array; + sharedSecret: ArrayBufferLike; + + constructor(ePublicKey: Uint8Array, sharedSecret: ArrayBufferLike) { + this.ePublicKey = ePublicKey; + this.sharedSecret = sharedSecret; + } +} + +class Ecdh { + publicKeyPoint: Uint8Array; + + constructor(publicKeyPoint: Uint8Array) { + this.publicKeyPoint = publicKeyPoint; + } + + static fromHex(publicKeyPointHex: string): Ecdh { + return new Ecdh(decodeHex(publicKeyPointHex)); + } + + ecdh(): EcdhEphemeral { + const bitLength = ((this.publicKeyPoint.length - 1) / 2) * 8; + if (bitLength !== 256) { + // NOTICE current only supports P256 + throw `Invalid EC public key point, bit length: ${bitLength}`; + } + const temp = createECDH(`secp${bitLength}r1`); + const ePublicKey = temp.generateKeys(); + const sharedSecret = temp.computeSecret(this.publicKeyPoint); + return new EcdhEphemeral(ePublicKey, sharedSecret); + } +} + +async function simpleKdf256( + input: ArrayBufferLike, +): Promise { + const SHA256 = "SHA-256"; + for (let i = 0; i < 8; i++) { + input = await crypto.subtle.digest(SHA256, input); + } + return input; +} + +function randomNonce(): ArrayBufferLike { + const buffer = new Uint8Array(12); + getRandomValues(buffer); + return buffer; +} + +class WrapKeyHeader { + kid: string; + enc: string; + ePubKey: string; + + constructor(kid: string, enc: string, ePubKey: string) { + this.kid = kid; + this.enc = enc; + this.ePubKey = ePubKey; + } +} + +class WrapKey { + header: WrapKeyHeader; + nonce: ArrayBuffer; + encryptedData: ArrayBuffer; + + constructor( + header: WrapKeyHeader, + nonce: ArrayBuffer, + encryptedData: ArrayBuffer, + ) { + this.header = header; + this.nonce = nonce; + this.encryptedData = encryptedData; + } + + toString(): string { + const headerJson = JSON.stringify(this.header); + return `WK:${encodeBase64Url(new TextEncoder().encode(headerJson))}.${ + encodeBase64Url(this.nonce) + }.${encodeBase64Url(this.encryptedData)}`; + } +} + +function concatUint8Array(a: Uint8Array, b: Uint8Array): Uint8Array { + const merge = new Uint8Array(a.byteLength + b.byteLength); + merge.set(a, 0); + merge.set(b, a.byteLength); + return merge; +} + +export async function encryptEcdhP256( + kid: string, + publicKeyPointHex: string, + data: ArrayBuffer, +) { + const ENC_AES256_GCM_P256 = "aes256-gcm-p256"; + const ecdh = Ecdh.fromHex(publicKeyPointHex); + const ecdhEphemeral = ecdh.ecdh(); + const key = await simpleKdf256(ecdhEphemeral.sharedSecret); + const nonce = randomNonce(); + + const derPrefix = decodeHex( + "3059301306072a8648ce3d020106082a8648ce3d030107034200", + ); + const der = concatUint8Array(derPrefix, ecdhEphemeral.ePublicKey); + const wrapkeyHeader = new WrapKeyHeader( + kid, + ENC_AES256_GCM_P256, + encodeBase64Url(der), + ); + const cryptoKey = await crypto.subtle.importKey( + "raw", + key, + "AES-GCM", + false, + ["encrypt"], + ); + const encryptedData = await crypto.subtle.encrypt( + { name: "AES-GCM", iv: nonce }, + cryptoKey, + data, + ); + return new WrapKey(wrapkeyHeader, nonce, encryptedData); +}