Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for importKey('jwk'). #183

Merged
merged 1 commit into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/cipherSuite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,8 @@ export class CipherSuite {
* @throws {@link DeserializeError}
*/
public async importKey(
format: "raw",
key: ArrayBuffer,
format: "raw" | "jwk",
key: ArrayBuffer | JsonWebKey,
isPublic = true,
): Promise<CryptoKey> {
await this.setup();
Expand Down
4 changes: 2 additions & 2 deletions src/interfaces/kemInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export interface KemInterface {
* Imports a key for the KEM.
*/
importKey(
format: "raw",
key: ArrayBuffer,
format: "raw" | "jwk",
key: ArrayBuffer | JsonWebKey,
isPublic: boolean,
): Promise<CryptoKey>;

Expand Down
4 changes: 2 additions & 2 deletions src/interfaces/kemPrimitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ export interface KemPrimitives {
deserializePublicKey(key: ArrayBuffer): Promise<CryptoKey>;

importKey(
format: "raw",
key: ArrayBuffer,
format: "raw" | "jwk",
key: ArrayBuffer | JsonWebKey,
isPublic: boolean,
): Promise<CryptoKey>;

Expand Down
4 changes: 2 additions & 2 deletions src/kems/dhkem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export class Dhkem extends WebCrypto implements KemInterface {
}

public async importKey(
format: "raw",
key: ArrayBuffer,
format: "raw" | "jwk",
key: ArrayBuffer | JsonWebKey,
isPublic: boolean,
): Promise<CryptoKey> {
try {
Expand Down
46 changes: 41 additions & 5 deletions src/kems/dhkemPrimitives/ec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,24 @@ export class Ec implements KemPrimitives {
}

public async importKey(
format: "raw",
key: ArrayBuffer,
format: "raw" | "jwk",
key: ArrayBuffer | JsonWebKey,
isPublic: boolean,
): Promise<CryptoKey> {
if (format !== "raw") {
throw new Error("Unsupported format");
if (format === "raw") {
return await this._importRawKey(key as ArrayBuffer, isPublic);
}
// jwk
if (key instanceof ArrayBuffer) {
throw new Error("Invalid jwk key format");
}
return await this._importJWK(key as JsonWebKey, isPublic);
}

private async _importRawKey(
key: ArrayBuffer,
isPublic: boolean,
): Promise<CryptoKey> {
if (isPublic && key.byteLength !== this._nPk) {
throw new Error("Invalid public key for the ciphersuite");
}
Expand All @@ -201,7 +212,7 @@ export class Ec implements KemPrimitives {
try {
if (isPublic) {
// return await this._api.importKey(format, key, this._alg, true, consts.KEM_USAGES);
return await this._api.importKey(format, key, this._alg, true, []);
return await this._api.importKey("raw", key, this._alg, true, []);
}
const k = new Uint8Array(key);
const pkcs8Key = new Uint8Array(this._pkcs8AlgId.length + k.length);
Expand All @@ -219,6 +230,31 @@ export class Ec implements KemPrimitives {
}
}

private async _importJWK(
key: JsonWebKey,
isPublic: boolean,
): Promise<CryptoKey> {
if (typeof key.crv === "undefined" || key.crv !== this._alg.namedCurve) {
throw new Error(`Invalid crv: ${key.crv}`);
}
if (isPublic) {
if (typeof key.d !== "undefined") {
throw new Error("Invalid key: `d` should not be set");
}
return await this._api.importKey("jwk", key, this._alg, true, []);
}
if (typeof key.d === "undefined") {
throw new Error("Invalid key: `d` not found");
}
return await this._api.importKey(
"jwk",
key,
this._alg,
true,
consts.KEM_USAGES,
);
}

public async derivePublicKey(key: CryptoKey): Promise<CryptoKey> {
const jwk = await this._api.exportKey("jwk", key);
delete jwk["d"];
Expand Down
57 changes: 51 additions & 6 deletions src/kems/dhkemPrimitives/x25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { KdfInterface } from "../../interfaces/kdfInterface.ts";
import { XCryptoKey } from "../../xCryptoKey.ts";

import * as consts from "../../consts.ts";
import { base64UrlToBytes } from "../../utils/misc.ts";

const ALG_NAME = "X25519";

Expand All @@ -29,14 +30,18 @@ export class X25519 implements KemPrimitives {
}

public async importKey(
format: "raw",
key: ArrayBuffer,
format: "raw" | "jwk",
key: ArrayBuffer | JsonWebKey,
isPublic: boolean,
): Promise<CryptoKey> {
if (format !== "raw") {
throw new Error("Unsupported format");
if (format === "raw") {
return await this._importRawKey(key as ArrayBuffer, isPublic);
}
// jwk
if (key instanceof ArrayBuffer) {
throw new Error("Invalid jwk key format");
}
return await this._importKey(key, isPublic);
return await this._importJWK(key as JsonWebKey, isPublic);
}

public async derivePublicKey(key: CryptoKey): Promise<CryptoKey> {
Expand Down Expand Up @@ -89,7 +94,10 @@ export class X25519 implements KemPrimitives {
});
}

private _importKey(key: ArrayBuffer, isPublic: boolean): Promise<CryptoKey> {
private _importRawKey(
key: ArrayBuffer,
isPublic: boolean,
): Promise<CryptoKey> {
return new Promise((resolve, reject) => {
if (isPublic && key.byteLength !== this._nPk) {
reject(new Error("Invalid public key for the ciphersuite"));
Expand All @@ -107,6 +115,43 @@ export class X25519 implements KemPrimitives {
});
}

private _importJWK(key: JsonWebKey, isPublic: boolean): Promise<CryptoKey> {
return new Promise((resolve, reject) => {
if (typeof key.kty === "undefined" || key.kty !== "OKP") {
reject(new Error(`Invalid kty: ${key.kty}`));
}
if (typeof key.crv === "undefined" || key.crv !== "X25519") {
reject(new Error(`Invalid crv: ${key.crv}`));
}
if (isPublic) {
if (typeof key.d !== "undefined") {
reject(new Error("Invalid key: `d` should not be set"));
}
if (typeof key.x === "undefined") {
reject(new Error("Invalid key: `x` not found"));
}
resolve(
new XCryptoKey(
ALG_NAME,
base64UrlToBytes(key.x as string),
"public",
),
);
} else {
if (typeof key.d !== "string") {
reject(new Error("Invalid key: `d` not found"));
}
resolve(
new XCryptoKey(
ALG_NAME,
base64UrlToBytes(key.d as string),
"private",
),
);
}
});
}

private _derivePublicKey(k: XCryptoKey): Promise<CryptoKey> {
return new Promise((resolve) => {
const pk = x25519.getPublicKey(k.key);
Expand Down
57 changes: 51 additions & 6 deletions src/kems/dhkemPrimitives/x448.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { KdfInterface } from "../../interfaces/kdfInterface.ts";
import { XCryptoKey } from "../../xCryptoKey.ts";

import * as consts from "../../consts.ts";
import { base64UrlToBytes } from "../../utils/misc.ts";

const ALG_NAME = "X448";

Expand All @@ -29,14 +30,18 @@ export class X448 implements KemPrimitives {
}

public async importKey(
format: "raw",
key: ArrayBuffer,
format: "raw" | "jwk",
key: ArrayBuffer | JsonWebKey,
isPublic: boolean,
): Promise<CryptoKey> {
if (format !== "raw") {
throw new Error("Unsupported format");
if (format === "raw") {
return await this._importRawKey(key as ArrayBuffer, isPublic);
}
// jwk
if (key instanceof ArrayBuffer) {
throw new Error("Invalid jwk key format");
}
return await this._importKey(key, isPublic);
return await this._importJWK(key as JsonWebKey, isPublic);
}

public async derivePublicKey(key: CryptoKey): Promise<CryptoKey> {
Expand Down Expand Up @@ -89,7 +94,10 @@ export class X448 implements KemPrimitives {
});
}

private _importKey(key: ArrayBuffer, isPublic: boolean): Promise<CryptoKey> {
private _importRawKey(
key: ArrayBuffer,
isPublic: boolean,
): Promise<CryptoKey> {
return new Promise((resolve, reject) => {
if (isPublic && key.byteLength !== this._nPk) {
reject(new Error("Invalid public key for the ciphersuite"));
Expand All @@ -107,6 +115,43 @@ export class X448 implements KemPrimitives {
});
}

private _importJWK(key: JsonWebKey, isPublic: boolean): Promise<CryptoKey> {
return new Promise((resolve, reject) => {
if (key.kty !== "OKP") {
reject(new Error(`Invalid kty: ${key.kty}`));
}
if (key.crv !== "X448") {
reject(new Error(`Invalid crv: ${key.crv}`));
}
if (isPublic) {
if (typeof key.d !== "undefined") {
reject(new Error("Invalid key: `d` should not be set"));
}
if (typeof key.x !== "string") {
reject(new Error("Invalid key: `x` not found"));
}
resolve(
new XCryptoKey(
ALG_NAME,
base64UrlToBytes(key.x as string),
"public",
),
);
} else {
if (typeof key.d !== "string") {
reject(new Error("Invalid key: `d` not found"));
}
resolve(
new XCryptoKey(
ALG_NAME,
base64UrlToBytes(key.d as string),
"private",
),
);
}
});
}

private _derivePublicKey(k: XCryptoKey): Promise<CryptoKey> {
return new Promise((resolve) => {
const pk = x448.getPublicKey(k.key);
Expand Down
15 changes: 14 additions & 1 deletion src/utils/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,19 @@ export function hexToBytes(v: string): Uint8Array {
/**
* Converts bytes to hex string.
*/
export function bytesToHex(v: Uint8Array) {
export function bytesToHex(v: Uint8Array): string {
return [...v].map((x) => x.toString(16).padStart(2, "0")).join("");
}

/**
* Decodes Base64Url-encoded data.
*/
export function base64UrlToBytes(v: string): Uint8Array {
const base64 = v.replace(/-/g, "+").replace(/_/g, "/");
const byteString = atob(base64);
const ret = new Uint8Array(byteString.length);
for (let i = 0; i < byteString.length; i++) {
ret[i] = byteString.charCodeAt(i);
}
return ret;
}
3 changes: 3 additions & 0 deletions src/xCryptoKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@ export class XCryptoKey implements CryptoKey {
this.key = key;
this.type = type;
this.algorithm = { name: name };
if (type === "public") {
this.usages = [];
}
}
}
Loading
Loading