mirror of
https://github.com/Blah-IM/typescript-core.git
synced 2025-04-30 16:21:10 +00:00
feat: encode private key with a password
This commit is contained in:
parent
9ff4d6da44
commit
ac711a63e1
3 changed files with 129 additions and 30 deletions
|
@ -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);
|
||||
|
|
|
@ -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
29
crypto/pbkdf2.ts
Normal 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;
|
||||
}
|
Loading…
Add table
Reference in a new issue