-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement key utils module (#15)
* implement key utils module * fix npm test
- Loading branch information
Showing
11 changed files
with
213 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export type PemCryptoKeyPair = { | ||
publicKey: string; | ||
privateKey: string; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { encodeBase64 } from "../utils/encoding.ts"; | ||
import type { PemCryptoKeyPair } from "./PemCryptoKeyPair.ts"; | ||
|
||
const formatPEM = (base64: string, type: string) => { | ||
const pemHeader = `-----BEGIN ${type}-----`; | ||
const pemFooter = `-----END ${type}-----`; | ||
const pemBody = base64.match(/.{1,64}/g)?.join("\n") ?? ""; | ||
return `${pemHeader}\n${pemBody}\n${pemFooter}`; | ||
}; | ||
|
||
/** | ||
* Export a CryptoKeyPair to PEM strings including the PEM header and footer. | ||
*/ | ||
export async function exportKeyPairToPEM( | ||
keyPair: CryptoKeyPair, | ||
): Promise<PemCryptoKeyPair> { | ||
const [spki, pkcs8] = await Promise.all([ | ||
crypto.subtle.exportKey("spki", keyPair.publicKey), | ||
crypto.subtle.exportKey("pkcs8", keyPair.privateKey), | ||
]); | ||
|
||
const publicKeyPEM = formatPEM(encodeBase64(spki), "PUBLIC KEY"); | ||
const privateKeyPEM = formatPEM(encodeBase64(pkcs8), "PRIVATE KEY"); | ||
|
||
return { | ||
publicKey: publicKeyPEM, | ||
privateKey: privateKeyPEM, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { extractFirstPemObject } from "../utils/pem.ts"; | ||
|
||
async function derivePublicKey(privateKey: CryptoKey): Promise<CryptoKey> { | ||
// d contains the private info of the key | ||
const { d: _discardedPrivateInfo, ...jwkPublic } = { | ||
...await crypto.subtle.exportKey("jwk", privateKey), | ||
key_ops: ["verify"], | ||
}; | ||
|
||
// Import the modified JWK as a public key | ||
return crypto.subtle.importKey( | ||
"jwk", | ||
jwkPublic, | ||
{ | ||
name: "ECDSA", | ||
namedCurve: "P-256", | ||
}, | ||
true, | ||
["verify"], | ||
); | ||
} | ||
|
||
/** | ||
* Import the private key in PEM format, derive its public key and return the `Promise<CryptoKeyPair>`. | ||
*/ | ||
export async function importKeyPairFromPemPrivateKey( | ||
pemPrivateKey: string, | ||
): Promise<CryptoKeyPair> { | ||
const privateKey = await crypto.subtle.importKey( | ||
"pkcs8", | ||
extractFirstPemObject(pemPrivateKey), | ||
{ | ||
name: "ECDSA", | ||
namedCurve: "P-256", | ||
}, | ||
true, | ||
["sign"], | ||
); | ||
|
||
return { | ||
privateKey, | ||
publicKey: await derivePublicKey(privateKey), | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { describe, expect, it } from "../../test_deps.ts"; | ||
import { generateKeyPair } from "../utils/crypto.ts"; | ||
import type { PemCryptoKeyPair } from "./mod.ts"; | ||
import * as CryptoKeyUtils from "./mod.ts"; | ||
|
||
describe("CryptoKeyUtils", () => { | ||
it("should export and import to the same key pair", async () => { | ||
const keyPair = await generateKeyPair(); | ||
const pemKeyPair = await CryptoKeyUtils.exportKeyPairToPEM(keyPair); | ||
expect(pemKeyPair.privateKey).toMatch( | ||
/^-----BEGIN PRIVATE KEY-----\n.+?\n-----END PRIVATE KEY-----$/s, | ||
); | ||
expect(pemKeyPair.publicKey).toMatch( | ||
/^-----BEGIN PUBLIC KEY-----\n.+?\n-----END PUBLIC KEY-----$/s, | ||
); | ||
|
||
const newKeyPair = await CryptoKeyUtils.importKeyPairFromPemPrivateKey( | ||
pemKeyPair.privateKey, | ||
); | ||
expect(newKeyPair.privateKey).not.toBe(keyPair.privateKey); | ||
expect(newKeyPair.publicKey).not.toBe(keyPair.publicKey); | ||
|
||
const [newPrivateKeyJwk, newPublicKeyJwk, privateKeyJwk, publicKeyJwk] = | ||
await Promise.all([ | ||
crypto.subtle.exportKey("jwk", newKeyPair.privateKey), | ||
crypto.subtle.exportKey("jwk", newKeyPair.publicKey), | ||
crypto.subtle.exportKey("jwk", keyPair.privateKey), | ||
crypto.subtle.exportKey("jwk", keyPair.publicKey), | ||
]); | ||
|
||
expect(newPrivateKeyJwk).toEqual(privateKeyJwk); | ||
expect(newPublicKeyJwk).toEqual(publicKeyJwk); | ||
}); | ||
|
||
it("should import and export to the same key pair pem", async () => { | ||
/** | ||
* generated from this command: | ||
* ``` | ||
* PRIVATE_KEY=$(openssl ecparam -name prime256v1 -genkey -noout | openssl pkcs8 -topk8 -nocrypt -outform PEM) && \ | ||
* echo "$PRIVATE_KEY" && \ | ||
* echo "$PRIVATE_KEY" | openssl ec -pubout | ||
* ``` | ||
*/ | ||
const pemKeyPair: PemCryptoKeyPair = { | ||
privateKey: `-----BEGIN PRIVATE KEY----- | ||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQghI1IlgiYmppCyzuK | ||
EsV2SkE0+o+Rd0tlIuCXaHk2pRuhRANCAAQynuucB8UmeBGFdoTbr7BCj1gzqFnu | ||
opWatNSFAi7oVa9k83PoyNlS88jcJ9E5D1WyFu9N1OUippPJ/MdZ+lq6 | ||
-----END PRIVATE KEY-----`, | ||
publicKey: `-----BEGIN PUBLIC KEY----- | ||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMp7rnAfFJngRhXaE26+wQo9YM6hZ | ||
7qKVmrTUhQIu6FWvZPNz6MjZUvPI3CfROQ9VshbvTdTlIqaTyfzHWfpaug== | ||
-----END PUBLIC KEY-----`, | ||
}; | ||
|
||
const keyPair = await CryptoKeyUtils.importKeyPairFromPemPrivateKey( | ||
pemKeyPair.privateKey, | ||
); | ||
|
||
expect(await CryptoKeyUtils.exportKeyPairToPEM(keyPair)).toEqual( | ||
pemKeyPair, | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/** | ||
* @module | ||
* | ||
* Utilities functions for handling CryptoKeys. | ||
*/ | ||
|
||
export * from "./exportKeyPairToPem.ts"; | ||
export * from "./importKeyPairFromPemPrivateKey.ts"; | ||
export * from "./PemCryptoKeyPair.ts"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,39 @@ | ||
export const encodeBase64Url = ( | ||
input: string | ArrayBuffer | Uint8Array, | ||
): string => { | ||
const bytes: string = (() => { | ||
): string => | ||
encodeBase64(input) | ||
// https://github.com/denoland/std/pull/3682#issuecomment-2417603682 | ||
.replace(/=?=$/, "") | ||
.replace(/\+/g, "-") | ||
.replace(/\//g, "_"); | ||
|
||
export const encodeBase64 = (input: ArrayBuffer | string | Uint8Array) => { | ||
const str: string = (() => { | ||
if (typeof input === "string") return input; | ||
return String.fromCharCode( | ||
...(input instanceof ArrayBuffer ? new Uint8Array(input) : input), | ||
); | ||
})(); | ||
return btoa(str); | ||
}; | ||
|
||
return btoa(bytes) | ||
// https://github.com/denoland/std/pull/3682#issuecomment-2417603682 | ||
.replace(/=?=$/, "") | ||
.replace(/\+/g, "-") | ||
.replace(/\//g, "_"); | ||
export const decodeBase64 = (input: string): Uint8Array => { | ||
const binaryString = atob(input); | ||
|
||
const bytes = Uint8Array.from( | ||
{ length: binaryString.length }, | ||
(_, i) => binaryString.charCodeAt(i), | ||
); | ||
|
||
return bytes; | ||
}; | ||
|
||
/* unused | ||
export const decodeBase64Url = (input: string): Uint8Array => | ||
decodeBase64( | ||
input | ||
.replace(/-/g, "+") | ||
.replace(/_/g, "/") | ||
.padEnd(input.length + (4 - (input.length % 4)) % 4, "="), | ||
); | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { decodeBase64 } from "./encoding.ts"; | ||
|
||
export const extractFirstPemObject = (pem: string): Uint8Array => | ||
decodeBase64( | ||
pem.replace(/-----BEGIN [^-]+-----/, "") | ||
.replace(/-----END [^-]+-----.*$/s, "") | ||
.replaceAll(/\s/g, ""), | ||
); |