import * as secp from "@noble/secp256k1"; import asn1 from "asn1.js"; interface CurveOptions { curve: "secp256k1"; curveParameters: readonly number[]; privatePEMOptions: { label: string }; publicPEMOptions: { label: string }; } interface PrivateKeyPKCS1 { parameters: readonly number[]; privateKey: Buffer; publicKey?: { data: Buffer; unused: number; }; version: number; } interface PrivateKeyPKCS8 { privateKey: PrivateKeyPKCS1; privateKeyAlgorithm: { curve: readonly number[]; ecPublicKey: number[]; }; version: number; } const curves = { secp256k1: { curve: "secp256k1", curveParameters: [1, 3, 132, 0, 10], privatePEMOptions: { label: "EC PRIVATE KEY" }, publicPEMOptions: { label: "PUBLIC KEY" }, }, } as const satisfies Record; type KeyFormat = "der" | "pem" | "raw"; /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return */ const ECPrivateKeyASN = asn1.define("ECPrivateKey", function (this: any) { this.seq().obj( this.key("version").int(), this.key("privateKey").octstr(), this.key("parameters").explicit(0).objid().optional(), this.key("publicKey").explicit(1).bitstr().optional(), ); }); const ECPrivateKey8ASN = asn1.define("ECPrivateKey", function (this: any) { this.seq().obj( this.key("version").int(), this.key("privateKeyAlgorithm") .seq() .obj(this.key("ecPublicKey").objid(), this.key("curve").objid()), this.key("privateKey").octstr().contains(ECPrivateKeyASN), this.key("attributes").explicit(0).bitstr().optional(), ); }); const SubjectPublicKeyInfoASN = asn1.define( "SubjectPublicKeyInfo", function (this: any) { this.seq().obj( this.key("algorithm") .seq() .obj(this.key("id").objid(), this.key("curve").objid()), this.key("pub").bitstr(), ); }, ); export class KeyEncoder { static ECPrivateKey8ASN = ECPrivateKey8ASN; static ECPrivateKeyASN = ECPrivateKeyASN; static SubjectPublicKeyInfoASN = SubjectPublicKeyInfoASN; algorithmID: number[]; options: CurveOptions; constructor(options: CurveOptions | string) { if (typeof options === "string") { if (options !== "secp256k1") { throw new Error(`Unknown curve ${options}`); } options = curves[options]; } this.options = options; this.algorithmID = [1, 2, 840, 10_045, 2, 1]; } encodePrivate( privateKey: Buffer | string, originalFormat: KeyFormat, destinationFormat: KeyFormat, destinationFormatType: "pkcs1" | "pkcs8" = "pkcs1", ): string { let privateKeyObject: PrivateKeyPKCS1; /* Parse the incoming private key and convert it to a private key object */ switch (originalFormat) { case "der": { if (typeof privateKey !== "string") { // do nothing } else if (typeof privateKey === "string") { privateKey = Buffer.from(privateKey, "hex"); } else { throw new TypeError("private key must be a buffer or a string"); } privateKeyObject = ECPrivateKeyASN.decode(privateKey, "der"); break; } case "pem": { if (typeof privateKey !== "string") { throw new TypeError("private key must be a string"); } privateKeyObject = ECPrivateKeyASN.decode( privateKey, "pem", this.options.privatePEMOptions, ); break; } case "raw": { if (typeof privateKey !== "string") { throw new TypeError("private key must be a string"); } const rawPublicKey = secp.etc.bytesToHex( secp.getPublicKey(privateKey, false), ); privateKeyObject = this.privateKeyObject(privateKey, rawPublicKey); break; } default: { throw new Error("invalid private key format"); } } /* Export the private key object to the desired format */ switch (destinationFormat) { case "der": { return ECPrivateKeyASN.encode(privateKeyObject, "der").toString("hex"); } case "pem": { return destinationFormatType === "pkcs1" ? ECPrivateKeyASN.encode( privateKeyObject, "pem", this.options.privatePEMOptions, ) : ECPrivateKey8ASN.encode( this.PKCS1toPKCS8(privateKeyObject), "pem", { ...this.options.privatePEMOptions, label: "PRIVATE KEY", }, ); } case "raw": { return privateKeyObject.privateKey.toString("hex"); } default: { throw new Error("invalid destination format for private key"); } } } encodePublic( publicKey: Buffer | string, originalFormat: KeyFormat, destinationFormat: KeyFormat, ): string { let publicKeyObject; /* Parse the incoming public key and convert it to a public key object */ switch (originalFormat) { case "der": { if (typeof publicKey !== "string") { // do nothing } else if (typeof publicKey === "string") { publicKey = Buffer.from(publicKey, "hex"); } else { throw new TypeError("public key must be a buffer or a string"); } publicKeyObject = SubjectPublicKeyInfoASN.decode(publicKey, "der"); break; } case "pem": { if (typeof publicKey !== "string") { throw new TypeError("public key must be a string"); } publicKeyObject = SubjectPublicKeyInfoASN.decode( publicKey, "pem", this.options.publicPEMOptions, ); break; } case "raw": { if (typeof publicKey !== "string") { throw new TypeError("public key must be a string"); } publicKeyObject = this.publicKeyObject(publicKey); break; } default: { throw new Error("invalid public key format"); } } /* Export the private key object to the desired format */ switch (destinationFormat) { case "der": { return SubjectPublicKeyInfoASN.encode(publicKeyObject, "der").toString( "hex", ); } case "pem": { return SubjectPublicKeyInfoASN.encode( publicKeyObject, "pem", this.options.publicPEMOptions, ); } case "raw": { return publicKeyObject.pub.data.toString("hex"); } default: { throw new Error("invalid destination format for public key"); } } } privateKeyObject(rawPrivateKey: string, rawPublicKey: string) { const privateKeyObject: PrivateKeyPKCS1 = { parameters: this.options.curveParameters, privateKey: Buffer.from(rawPrivateKey, "hex"), version: 1, }; if (rawPublicKey) { privateKeyObject.publicKey = { data: Buffer.from(rawPublicKey, "hex"), unused: 0, }; } return privateKeyObject; } publicKeyObject(rawPublicKey: string) { return { algorithm: { curve: this.options.curveParameters, id: this.algorithmID, }, pub: { data: Buffer.from(rawPublicKey, "hex"), unused: 0, }, }; } private PKCS1toPKCS8(privateKey: PrivateKeyPKCS1): PrivateKeyPKCS8 { return { privateKey, privateKeyAlgorithm: { curve: privateKey.parameters, ecPublicKey: this.algorithmID, }, version: 0, }; } } /* eslint-enable */