feat: add deno-wrapkey-mod.ts

This commit is contained in:
2025-01-21 01:19:46 +08:00
parent 3645ac9f32
commit 6106c68a03

View File

@@ -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<ArrayBuffer> {
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);
}