feat: encode private key with a password

This commit is contained in:
Shibo Lyu 2024-10-11 00:42:52 +08:00
parent 9ff4d6da44
commit ac711a63e1
3 changed files with 129 additions and 30 deletions

View file

@ -14,6 +14,14 @@ Deno.test("encode & decode keypair", async () => {
expect(decoded.id).toBe(keypair.id);
});
Deno.test("encode & decode keypair w/ password", async () => {
const password = "password";
const encoded = await keypair.encode(password);
const decoded = await BlahKeyPair.fromEncoded(encoded, password);
expect(decoded.id).toBe(keypair.id);
});
Deno.test("sign & verify payload", async () => {
const payload = { foo: "bar", baz: 123 };
const signedPayload = await keypair.signPayload(payload);

View file

@ -1,13 +1,22 @@
import canonicalize from "./canonicalize.ts";
import { hexToBuf } from "./mod.ts";
import { pbkdf2Key } from "./pbkdf2.ts";
import { BlahPublicKey } from "./publicKey.ts";
import type { BlahPayloadSignee, BlahSignedPayload } from "./signedPayload.ts";
import { bufToHex } from "./utils.ts";
export type EncodedBlahKeyPair = {
v: "0";
id: string;
privateKey: JsonWebKey;
};
export type EncodedBlahKeyPair =
& {
v: "0";
id: string;
}
& ({
privateKey: JsonWebKey;
} | {
passwordProtectedPrivateKey: string;
iv: string;
salt: string;
});
export class BlahKeyPair {
publicKey: BlahPublicKey;
@ -28,45 +37,98 @@ export class BlahKeyPair {
this.privateKey = privateKey;
}
static async generate(
extractable: boolean = true,
additionalUsage: KeyUsage[] = [],
): Promise<BlahKeyPair> {
static async generate(extractable: boolean = true): Promise<BlahKeyPair> {
const { publicKey, privateKey } = await crypto.subtle.generateKey(
"Ed25519",
extractable,
[
"sign",
"verify",
...additionalUsage,
],
["sign", "verify"],
) as CryptoKeyPair;
const publicIdentity = await BlahPublicKey.fromPublicKey(publicKey);
return new BlahKeyPair(publicIdentity, privateKey);
}
static async fromEncoded(encoded: EncodedBlahKeyPair): Promise<BlahKeyPair> {
static async fromEncoded(
encoded: EncodedBlahKeyPair,
password?: string,
): Promise<BlahKeyPair> {
if (encoded.v !== "0") {
throw new Error("Unsupported version");
}
const publicIdentity = await BlahPublicKey.fromID(encoded.id);
const privateKey = await crypto.subtle.importKey(
"jwk",
encoded.privateKey,
{ name: "Ed25519" },
true,
["sign"],
);
return new BlahKeyPair(publicIdentity, privateKey);
const publicIdentity = await BlahPublicKey.fromID(encoded.id);
if ("passwordProtectedPrivateKey" in encoded) {
if (!password) {
throw new Error("Private key is password-protected.");
}
const derviedKey = await pbkdf2Key(password, encoded.salt);
const privateKey = await crypto.subtle.unwrapKey(
"pkcs8",
hexToBuf(encoded.passwordProtectedPrivateKey),
derviedKey,
{
name: "AES-GCM",
iv: hexToBuf(encoded.iv),
},
{ name: "Ed25519" },
true,
["sign"],
);
return new BlahKeyPair(publicIdentity, privateKey);
} else {
const privateKey = await crypto.subtle.importKey(
"jwk",
encoded.privateKey,
{ name: "Ed25519" },
true,
["sign"],
);
return new BlahKeyPair(publicIdentity, privateKey);
}
}
async encode(): Promise<EncodedBlahKeyPair> {
return {
v: "0",
id: this.publicKey.id,
privateKey: await crypto.subtle.exportKey("jwk", this.privateKey),
};
async encode(password?: string): Promise<EncodedBlahKeyPair> {
if (!this.privateKey.extractable) {
throw new Error("Private key is not extractable.");
}
if (password) {
const saltBuf = new Uint8Array(16);
crypto.getRandomValues(saltBuf);
const salt = bufToHex(saltBuf);
const ivBuf = new Uint8Array(12);
crypto.getRandomValues(ivBuf);
const iv = bufToHex(ivBuf);
const derviedKey = await pbkdf2Key(password, saltBuf);
const wrappedPrivateKey = await crypto.subtle.wrapKey(
"pkcs8",
this.privateKey,
derviedKey,
{
name: "AES-GCM",
iv: ivBuf,
},
);
return {
v: "0",
id: this.publicKey.id,
passwordProtectedPrivateKey: bufToHex(wrappedPrivateKey),
iv,
salt,
};
} else {
return {
v: "0",
id: this.publicKey.id,
privateKey: await crypto.subtle.exportKey("jwk", this.privateKey),
};
}
}
async signPayload<P>(

29
crypto/pbkdf2.ts Normal file
View file

@ -0,0 +1,29 @@
import { hexToBuf } from "./mod.ts";
export async function pbkdf2Key(
password: string,
salt: string | Uint8Array,
): Promise<CryptoKey> {
const passwordKey = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
{ name: "PBKDF2" },
false,
["deriveKey"],
);
const key = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: typeof salt === "string" ? hexToBuf(salt) : salt,
iterations: 250000,
hash: "SHA-256",
},
passwordKey,
{ name: "AES-GCM", length: 256 },
false,
["wrapKey", "unwrapKey"],
);
return key;
}