import { Integer, Null, ObjectIdentifier, OctetString, Sequence } from "asn1js"; function buffer(bytes: Uint8Array): ArrayBuffer { return bytes.buffer.slice( bytes.byteOffset, bytes.byteOffset + bytes.byteLength, ) as ArrayBuffer; } export function encryptedPkcs8Pem(bytes: Uint8Array): string { const binary = Array.from(bytes, (value) => String.fromCharCode(value)).join( "", ); const body = btoa(binary) .match(/.{1,64}/gu) ?.join("\n") ?? ""; return `-----BEGIN ENCRYPTED PRIVATE KEY-----\n${body}\n-----END ENCRYPTED PRIVATE KEY-----`; } export async function encryptedPkcs8(password: string): Promise { const pair = await crypto.subtle.generateKey( { name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"], ); const privateKey = new Uint8Array( await crypto.subtle.exportKey("pkcs8", pair.privateKey), ); try { return await encryptedPbes2Payload(password, privateKey); } finally { privateKey.fill(0); } } export async function encryptedPbes2Payload( password: string, plaintext: Uint8Array, ): Promise { const salt = Uint8Array.from({ length: 16 }, (_, index) => index + 1); const iv = Uint8Array.from({ length: 16 }, (_, index) => 32 - index); const material = await crypto.subtle.importKey( "raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveKey"], ); const key = await crypto.subtle.deriveKey( { name: "PBKDF2", hash: "SHA-256", salt: buffer(salt), iterations: 12_000, }, material, { name: "AES-CBC", length: 256 }, false, ["encrypt"], ); const ciphertext = new Uint8Array( await crypto.subtle.encrypt( { name: "AES-CBC", iv: buffer(iv) }, key, buffer(plaintext), ), ); const encoded = new Sequence({ value: [ new Sequence({ value: [ new ObjectIdentifier({ value: "1.2.840.113549.1.5.13" }), new Sequence({ value: [ new Sequence({ value: [ new ObjectIdentifier({ value: "1.2.840.113549.1.5.12" }), new Sequence({ value: [ new OctetString({ valueHex: buffer(salt) }), new Integer({ value: 12_000 }), new Sequence({ value: [ new ObjectIdentifier({ value: "1.2.840.113549.2.9" }), new Null(), ], }), ], }), ], }), new Sequence({ value: [ new ObjectIdentifier({ value: "2.16.840.1.101.3.4.1.42" }), new OctetString({ valueHex: buffer(iv) }), ], }), ], }), ], }), new OctetString({ valueHex: buffer(ciphertext) }), ], }); return new Uint8Array(encoded.toBER(false)); }