Skip to content

Commit

Permalink
feat: Add support for Ed25519 and X25519 algorithms
Browse files Browse the repository at this point in the history
  • Loading branch information
microshine committed May 28, 2024
1 parent 965012d commit a0cf289
Show file tree
Hide file tree
Showing 10 changed files with 504 additions and 0 deletions.
120 changes: 120 additions & 0 deletions src/mechs/ed25519/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import crypto from "crypto";
import { AsnConvert } from "@peculiar/asn1-schema";
import { Convert } from "pvtsutils";
import * as core from "webcrypto-core";
import { Ed25519CryptoKey } from "./crypto_key";
import { Ed25519PrivateKey } from "./private_key";
import { Ed25519PublicKey } from "./public_key";
import { CryptoKey } from "../../keys";

export class Ed25519Crypto {
public static privateKeyUsages: KeyUsage[] = ["sign", "deriveBits", "deriveKey"];
public static publicKeyUsages: KeyUsage[] = ["verify"];

public static async generateKey(algorithm: Algorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise<CryptoKeyPair> {
const type = algorithm.name.toLowerCase() as "ed25519";
const keys = crypto.generateKeyPairSync(type, {
publicKeyEncoding: {
format: "pem",
type: "spki",
},
privateKeyEncoding: {
format: "pem",
type: "pkcs8",
},
});

const keyAlg = {
name: type === "ed25519" ? "Ed25519" : "X25519",
};
const privateKeyUsages = keyUsages.filter((usage) => this.privateKeyUsages.includes(usage));
const publicKeyUsages = keyUsages.filter((usage) => this.publicKeyUsages.includes(usage));
return {
privateKey: new Ed25519PrivateKey(keyAlg, extractable, privateKeyUsages, keys.privateKey),
publicKey: new Ed25519PublicKey(keyAlg, true, publicKeyUsages, keys.publicKey),
};
}

public static async sign(algorithm: Algorithm, key: Ed25519PrivateKey, data: Uint8Array): Promise<ArrayBuffer> {
const signature = crypto.sign(null, Buffer.from(data), key.data);

return core.BufferSourceConverter.toArrayBuffer(signature);
}

public static async verify(algorithm: Algorithm, key: Ed25519PublicKey, signature: Uint8Array, data: Uint8Array): Promise<boolean> {
return crypto.verify(null, Buffer.from(data), key.data, signature);
}

public static async exportKey(format: KeyFormat, key: Ed25519CryptoKey): Promise<JsonWebKey | ArrayBuffer> {
switch (format) {
case "jwk":
return key.toJWK();
case "pkcs8": {
return core.PemConverter.toArrayBuffer(key.data.toString());
}
case "spki": {
return core.PemConverter.toArrayBuffer(key.data.toString());
}
case "raw": {
const jwk = key.toJWK();
return Convert.FromBase64Url(jwk.x!);
}
default:
return Promise.reject(new core.OperationError("format: Must be 'jwk', 'raw', pkcs8' or 'spki'"));
}
}

public static async importKey(format: KeyFormat, keyData: JsonWebKey | ArrayBuffer, algorithm: Algorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise<CryptoKey> {
switch (format) {
case "jwk": {
const jwk = keyData as JsonWebKey;
if (jwk.d) {
// private key
const privateData = new core.asn1.EdPrivateKey();
privateData.value = core.BufferSourceConverter.toArrayBuffer(Buffer.from(jwk.d, "base64url"));
const pkcs8 = new core.asn1.PrivateKeyInfo();
pkcs8.privateKeyAlgorithm.algorithm = algorithm.name.toLowerCase() === "ed25519"
? core.asn1.idEd25519
: core.asn1.idX25519;
pkcs8.privateKey = AsnConvert.serialize(privateData);
const raw = AsnConvert.serialize(pkcs8);
const pem = core.PemConverter.fromBufferSource(raw, "PRIVATE KEY");
return new Ed25519PrivateKey(algorithm, extractable, keyUsages, pem);
} else if (jwk.x) {
// public key
const pubKey = crypto.createPublicKey({
format: "jwk",
key: jwk as crypto.JsonWebKey,
});
const pem = pubKey.export({ format: "pem", type: "spki" }) as string;
return new Ed25519PublicKey(algorithm, extractable, keyUsages, pem);
} else {
throw new core.OperationError("keyData: Cannot import JWK. 'd' or 'x' must be presented");
}
}
case "pkcs8": {
const pem = core.PemConverter.fromBufferSource(keyData as ArrayBuffer, "PRIVATE KEY");
return new Ed25519PrivateKey(algorithm, extractable, keyUsages, pem);
}
case "spki": {
const pem = core.PemConverter.fromBufferSource(keyData as ArrayBuffer, "PUBLIC KEY");
return new Ed25519PublicKey(algorithm, extractable, keyUsages, pem);
}
case "raw": {
const raw = keyData as ArrayBuffer;
const key = crypto.createPublicKey({
format: "jwk",
key: {
kty: "OKP",
crv: algorithm.name.toLowerCase() === "ed25519" ? "Ed25519" : "X25519",
x: Convert.ToBase64Url(raw),
},
});
const pem = key.export({ format: "pem", type: "spki" }) as string;
return new Ed25519PublicKey(algorithm, extractable, keyUsages, pem);
}
default:
return Promise.reject(new core.OperationError("format: Must be 'jwk', 'raw', pkcs8' or 'spki'"));
}
}
}
21 changes: 21 additions & 0 deletions src/mechs/ed25519/crypto_key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { CryptoKey } from "../../keys";

export class Ed25519CryptoKey extends CryptoKey {

constructor(algorithm: Algorithm, extractable: boolean, usages: KeyUsage[], data: string) {
super();
this.algorithm = algorithm;
this.extractable = extractable;
this.usages = usages;
this.data = Buffer.from(data);
}

public toJWK(): JsonWebKey {
return {
kty: "OKP",
crv: this.algorithm.name,
key_ops: this.usages,
ext: this.extractable,
};
}
}
37 changes: 37 additions & 0 deletions src/mechs/ed25519/ed25519.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as core from "webcrypto-core";
import { Ed25519Crypto } from "./crypto";
import { Ed25519CryptoKey } from "./crypto_key";
import { Ed25519PrivateKey } from "./private_key";
import { Ed25519PublicKey } from "./public_key";
import { getCryptoKey, setCryptoKey } from "../storage";

export class Ed25519Provider extends core.Ed25519Provider {
public override async onGenerateKey(algorithm: Algorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise<CryptoKeyPair> {
const keys = await Ed25519Crypto.generateKey(algorithm, extractable, keyUsages);
return {
privateKey: setCryptoKey(keys.privateKey as Ed25519CryptoKey),
publicKey: setCryptoKey(keys.publicKey as Ed25519CryptoKey),
};
}

override async onSign(algorithm: Algorithm, key: Ed25519PrivateKey, data: ArrayBuffer): Promise<ArrayBuffer> {
const internalKey = getCryptoKey(key) as Ed25519PrivateKey;
const signature = Ed25519Crypto.sign(algorithm, internalKey, new Uint8Array(data));
return signature;
}

override onVerify(algorithm: Algorithm, key: Ed25519PublicKey, signature: ArrayBuffer, data: ArrayBuffer): Promise<boolean> {
const internalKey = getCryptoKey(key) as Ed25519PublicKey;
return Ed25519Crypto.verify(algorithm, internalKey, new Uint8Array(signature), new Uint8Array(data));
}

override async onExportKey(format: KeyFormat, key: Ed25519CryptoKey): Promise<JsonWebKey | ArrayBuffer> {
const internalKey = getCryptoKey(key) as Ed25519CryptoKey;
return Ed25519Crypto.exportKey(format, internalKey);
}

override async onImportKey(format: KeyFormat, keyData: JsonWebKey | ArrayBuffer, algorithm: Algorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise<core.CryptoKey> {
const internalKey = await Ed25519Crypto.importKey(format, keyData, algorithm, extractable, keyUsages);
return setCryptoKey(internalKey);
}
}
4 changes: 4 additions & 0 deletions src/mechs/ed25519/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./ed25519";
export * from "./x25519";
export * from "./private_key";
export * from "./public_key";
23 changes: 23 additions & 0 deletions src/mechs/ed25519/private_key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import crypto from "crypto";
import { AsnConvert } from "@peculiar/asn1-schema";
import * as core from "webcrypto-core";
import { Ed25519CryptoKey } from "./crypto_key";

export class Ed25519PrivateKey extends Ed25519CryptoKey {
public override type = "private" as const;

public override toJWK(): JsonWebKey {
const pubJwk = crypto.createPublicKey({
key: this.data,
format: "pem",
}).export({ format: "jwk" }) as JsonWebKey;
const raw = core.PemConverter.toUint8Array(this.data.toString());
const pkcs8 = AsnConvert.parse(raw, core.asn1.PrivateKeyInfo);
const d = AsnConvert.parse(pkcs8.privateKey, core.asn1.EdPrivateKey).value;
return {
...super.toJWK(),
...pubJwk,
d: Buffer.from(new Uint8Array(d)).toString("base64url"),
};
}
}
18 changes: 18 additions & 0 deletions src/mechs/ed25519/public_key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import crypto from "crypto";
import { Ed25519CryptoKey } from "./crypto_key";

export class Ed25519PublicKey extends Ed25519CryptoKey {
public override type = "public" as const;

public override toJWK(): JsonWebKey {
const jwk = crypto.createPublicKey({
key: this.data,
format: "pem",
}).export({ format: "jwk" }) as JsonWebKey;

return {
...super.toJWK(),
...jwk,
};
}
}
54 changes: 54 additions & 0 deletions src/mechs/ed25519/x25519.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import crypto from "crypto";
import * as core from "webcrypto-core";
import { Ed25519Crypto } from "./crypto";
import { Ed25519CryptoKey } from "./crypto_key";
import { CryptoKey } from "../../keys";
import { getCryptoKey, setCryptoKey } from "../storage";

export class X25519Provider extends core.X25519Provider {
public override async onGenerateKey(algorithm: Algorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise<CryptoKeyPair> {
const keys = await Ed25519Crypto.generateKey(algorithm, extractable, keyUsages);
return {
privateKey: setCryptoKey(keys.privateKey as Ed25519CryptoKey),
publicKey: setCryptoKey(keys.publicKey as Ed25519CryptoKey),
};
}

public override async onDeriveBits(algorithm: EcdhKeyDeriveParams, baseKey: Ed25519CryptoKey, length: number): Promise<ArrayBuffer> {
const internalBaseKey = getCryptoKey(baseKey);
const internalPublicKey = getCryptoKey(algorithm.public);
const publicKey = crypto.createPublicKey({
key: internalPublicKey.data.toString(),
format: "pem",
type: "spki",
});
const privateKey = crypto.createPrivateKey({
key: internalBaseKey.data.toString(),
format: "pem",
type: "pkcs8",
});
const bits = crypto.diffieHellman({
publicKey,
privateKey,
});

return new Uint8Array(bits).buffer.slice(0, length >> 3);
}

public override async onExportKey(format: KeyFormat, key: Ed25519CryptoKey): Promise<JsonWebKey | ArrayBuffer> {
const internalKey = getCryptoKey(key);
return Ed25519Crypto.exportKey(format, internalKey as Ed25519CryptoKey);
}

public override async onImportKey(format: KeyFormat, keyData: JsonWebKey | ArrayBuffer, algorithm: Algorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise<core.CryptoKey> {
const key = await Ed25519Crypto.importKey(format, keyData, algorithm, extractable, keyUsages);
return setCryptoKey(key);
}

override checkCryptoKey(key: CryptoKey, keyUsage?: KeyUsage | undefined): void {
super.checkCryptoKey(key, keyUsage);
if (!(getCryptoKey(key) instanceof Ed25519CryptoKey)) {
throw new TypeError("key: Is not a Ed25519CryptoKey");
}
}
}
1 change: 1 addition & 0 deletions src/mechs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from "./des";
export * from "./rsa";
export * from "./ec";
export * from "./ed";
export * from "./ed25519";
export * from "./sha";
export * from "./pbkdf";
export * from "./hmac";
Expand Down
4 changes: 4 additions & 0 deletions src/subtle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
EcdsaProvider, HkdfProvider,
EdDsaProvider,
EcdhEsProvider,
Ed25519Provider, X25519Provider,
HmacProvider,
Pbkdf2Provider,
RsaEsProvider, RsaOaepProvider, RsaPssProvider, RsaSsaProvider,
Expand Down Expand Up @@ -95,6 +96,9 @@ export class SubtleCrypto extends core.SubtleCrypto {
//#region ECDH-ES
this.providers.set(new EcdhEsProvider());
//#endregion

this.providers.set(new Ed25519Provider());
this.providers.set(new X25519Provider());
}
}
}
Loading

0 comments on commit a0cf289

Please sign in to comment.