Skip to content

Commit

Permalink
Merge pull request #68 from PeculiarVentures/ed25519
Browse files Browse the repository at this point in the history
Add x25519 and ed25519 support to ED module
  • Loading branch information
microshine authored May 28, 2024
2 parents 3312852 + 24e4e7c commit d96e153
Show file tree
Hide file tree
Showing 9 changed files with 593 additions and 242 deletions.
20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,19 @@
},
"devDependencies": {
"@types/mocha": "^10.0.6",
"@types/node": "^20.11.5",
"@types/node": "^20.12.12",
"@typescript-eslint/eslint-plugin": "^7.11.0",
"@typescript-eslint/parser": "^7.11.0",
"coveralls": "^3.1.1",
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.17.0",
"eslint": "^8.56.0",
"mocha": "^10.2.0",
"eslint": "^8.57.0",
"mocha": "^10.4.0",
"nyc": "^15.1.0",
"rimraf": "^5.0.5",
"rollup": "^4.9.6",
"rollup-plugin-dts": "^6.1.0",
"rimraf": "^5.0.7",
"rollup": "^4.18.0",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-typescript2": "^0.36.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
"typescript": "^5.4.5"
},
"author": "PeculiarVentures",
"license": "MIT",
Expand Down Expand Up @@ -91,4 +91,4 @@
"test/**/*.ts"
]
}
}
}
15 changes: 15 additions & 0 deletions src/ed/ed25519.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ProviderCrypto } from "../provider";
import { ProviderKeyUsages } from "../types";

export abstract class Ed25519Provider extends ProviderCrypto {

public readonly name: string = "Ed25519";

public usages: ProviderKeyUsages = {
privateKey: ["sign"],
publicKey: ["verify"],
};

public abstract onSign(algorithm: Algorithm, key: CryptoKey, data: ArrayBuffer, ...args: any[]): Promise<ArrayBuffer>;
public abstract onVerify(algorithm: Algorithm, key: CryptoKey, signature: ArrayBuffer, data: ArrayBuffer, ...args: any[]): Promise<boolean>;
}
2 changes: 2 additions & 0 deletions src/ed/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "../ed/x25519";
export * from "../ed/ed25519";
18 changes: 18 additions & 0 deletions src/ed/x25519.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ProviderCrypto } from "../provider";
import { ProviderKeyUsages } from "../types";

export abstract class X25519Provider extends ProviderCrypto {

public readonly name: string = "X25519";

public usages: ProviderKeyUsages = {
privateKey: ["deriveKey", "deriveBits"],
publicKey: [],
};

public checkAlgorithmParams(algorithm: EcdhKeyDeriveParams): void {
this.checkRequiredProperty(algorithm, "public");
}

public abstract onDeriveBits(algorithm: EcdhKeyDeriveParams, baseKey: CryptoKey, length: number): Promise<ArrayBuffer>;
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from "./aes";
export * from "./des";
export * from "./rsa";
export * from "./ec";
export * from "./ed";
export * from "./hmac";
export * from "./pbkdf";
export * from "./hkdf";
Expand Down
37 changes: 28 additions & 9 deletions src/subtle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import { ProviderStorage } from "./storage";
import { HashedAlgorithm } from "./types";
import { CryptoKey } from './crypto_key';

const keyFormatMap: Record<KeyFormat, KeyType[]> = {
"jwk": ["private", "public", "secret"],
"pkcs8": ["private"],
"spki": ["public"],
"raw": ["secret", "public"]
};

const sourceBufferKeyFormats = ["pkcs8", "spki", "raw"];
export class SubtleCrypto implements globalThis.SubtleCrypto {

public static isHashedAlgorithm(data: any): data is HashedAlgorithm {
Expand Down Expand Up @@ -155,29 +163,40 @@ export class SubtleCrypto implements globalThis.SubtleCrypto {
const [format, key, ...params] = args;
this.checkCryptoKey(key);

if (!keyFormatMap[format as KeyFormat]) {
throw new TypeError("Invalid keyFormat argument");
}

if (!keyFormatMap[format as KeyFormat].includes(key.type)) {
throw new DOMException("The key is not of the expected type");
}

const provider = this.getProvider(key.algorithm.name);
const result = await provider.exportKey(format, key, ...params);

return result;
}
public async importKey(format: KeyFormat, keyData: JsonWebKey | BufferSource, algorithm: AlgorithmIdentifier, extractable: boolean, keyUsages: KeyUsage[], ...args: any[]): Promise<globalThis.CryptoKey>;

public async importKey(...args: any[]): Promise<globalThis.CryptoKey> {
this.checkRequiredArguments(args, 5, "importKey");
const [format, keyData, algorithm, extractable, keyUsages, ...params] = args;

const preparedAlgorithm = this.prepareAlgorithm(algorithm);
const provider = this.getProvider(preparedAlgorithm.name);

if (["pkcs8", "spki", "raw"].indexOf(format) !== -1) {
const preparedData = BufferSourceConverter.toArrayBuffer(keyData as BufferSource);

return provider.importKey(format, preparedData, { ...preparedAlgorithm, name: provider.name }, extractable, keyUsages, ...params);
} else {
if (!(keyData as JsonWebKey).kty) {
throw new TypeError("keyData: Is not JSON");
if (format === "jwk") {
if (typeof keyData !== "object" || !(keyData as JsonWebKey).kty) {
throw new TypeError("Key data must be an object for JWK import");
}
} else if (sourceBufferKeyFormats.includes(format)) {
if (!BufferSourceConverter.isBufferSource(keyData)) {
throw new TypeError("Key data must be a BufferSource for non-JWK formats");
}
} else {
throw new TypeError("The provided value is not of type '(ArrayBuffer or ArrayBufferView or JsonWebKey)'");
}
return provider.importKey(format, keyData as JsonWebKey, { ...preparedAlgorithm, name: provider.name }, extractable, keyUsages, ...params);

return provider.importKey(format, keyData, { ...preparedAlgorithm, name: provider.name }, extractable, keyUsages, ...params);
}

public async wrapKey(format: KeyFormat, key: globalThis.CryptoKey, wrappingKey: globalThis.CryptoKey, wrapAlgorithm: AlgorithmIdentifier, ...args: any[]): Promise<ArrayBuffer> {
Expand Down
201 changes: 199 additions & 2 deletions test/ed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AsnConvert, AsnSerializer } from "@peculiar/asn1-schema";
import * as assert from "assert";
import { Convert } from "pvtsutils";
import { EdPrivateKey, EdPublicKey, OneAsymmetricKey, PublicKeyInfo } from "../src/asn1";
import { CryptoKey, Ed25519Provider, X25519Provider } from "../src";

context("ED", () => {

Expand Down Expand Up @@ -29,7 +30,7 @@ context("ED", () => {

const keyInfo = AsnConvert.parse(Convert.FromBase64(pem), OneAsymmetricKey);
assert.strictEqual(keyInfo.publicKey, undefined);
const key = AsnConvert.parse(keyInfo.privateKey, EdPrivateKey)
const key = AsnConvert.parse(keyInfo.privateKey, EdPrivateKey);
const jwk = key.toJSON();

const key2 = new EdPrivateKey();
Expand All @@ -45,7 +46,7 @@ context("ED", () => {

const keyInfo = AsnConvert.parse(Convert.FromBase64(pem), OneAsymmetricKey);
assert.ok(keyInfo.publicKey);
const key = AsnConvert.parse(keyInfo.privateKey, EdPrivateKey)
const key = AsnConvert.parse(keyInfo.privateKey, EdPrivateKey);
const jwk = key.toJSON();

const key2 = new EdPrivateKey();
Expand All @@ -60,4 +61,200 @@ context("ED", () => {

});

context("Ed25519", () => {
class TestEd25519Provider extends Ed25519Provider {
public override async onGenerateKey(algorithm: Algorithm, extractable: boolean, keyUsages: KeyUsage[], ..._args: any[]): Promise<CryptoKeyPair> {
const privateKey = new CryptoKey();
privateKey.algorithm = { name: "Ed25519" };
privateKey.type = "private";
privateKey.extractable = extractable;
privateKey.usages = ["sign"];

const publicKey = new CryptoKey();
publicKey.algorithm = { name: "Ed25519" };
publicKey.type = "public";
publicKey.extractable = true;
publicKey.usages = ["verify"];

return {
privateKey,
publicKey,
};
}

public async onSign(algorithm: Algorithm, key: CryptoKey, data: ArrayBuffer, ...args: any[]): Promise<ArrayBuffer> {
return new ArrayBuffer(64);
}
public async onVerify(algorithm: Algorithm, key: CryptoKey, signature: ArrayBuffer, data: ArrayBuffer, ...args: any[]): Promise<boolean> {
return true;
}
}

const provider = new TestEd25519Provider();

context("generateKey", () => {
it("should generate key pair", async () => {
const keys = await provider.generateKey({
name: "Ed25519",
}, true, ["sign", "verify"]);
assert.ok("privateKey" in keys);
assert.ok("publicKey" in keys);
assert.strictEqual(keys.privateKey.algorithm.name, "Ed25519");
assert.strictEqual(keys.privateKey.type, "private");
assert.strictEqual(keys.privateKey.extractable, true);
assert.deepStrictEqual(keys.privateKey.usages, ["sign"]);
assert.strictEqual(keys.publicKey.algorithm.name, "Ed25519");
assert.strictEqual(keys.publicKey.type, "public");
assert.strictEqual(keys.publicKey.extractable, true);
assert.deepStrictEqual(keys.publicKey.usages, ["verify"]);
});
it("should throw error when algorithm is not correct", async () => {
await assert.rejects(provider.generateKey({
name: "RSASSA-PKCS1-v1_5",
}, true, ["sign", "verify"]), {
message: "Unrecognized name",
});
});
it("should throw error when keyUsages is not correct", async () => {
await assert.rejects(provider.generateKey({
name: "Ed25519",
}, true, ["encrypt", "decrypt"]), {
message: "Cannot create a key using the specified key usages",
});
});
});
context("sign", () => {
it("should sign data", async () => {
const keys = await provider.generateKey({
name: "Ed25519",
}, true, ["sign", "verify"]);
assert.ok("privateKey" in keys);
const signature = await provider.sign({
name: "Ed25519",
}, keys.privateKey, new ArrayBuffer(32));
});
it("should throw error when algorithm is not correct", async () => {
const keys = await provider.generateKey({
name: "Ed25519",
}, true, ["sign", "verify"]);
assert.ok("privateKey" in keys);
await assert.rejects(provider.sign({
name: "RSASSA-PKCS1-v1_5",
}, keys.privateKey, new ArrayBuffer(32)), {
message: "Unrecognized name",
});
});
});
context("verify", () => {
it("should verify signature", async () => {
const keys = await provider.generateKey({
name: "Ed25519",
}, true, ["sign", "verify"]);
assert.ok("privateKey" in keys);
const signature = new ArrayBuffer(64);
const res = await provider.verify({
name: "Ed25519",
}, keys.publicKey, signature, new ArrayBuffer(32));
assert.strictEqual(res, true);
});
it("should throw error when algorithm is not correct", async () => {
const keys = await provider.generateKey({
name: "Ed25519",
}, true, ["sign", "verify"]);
assert.ok("privateKey" in keys);
const signature = new ArrayBuffer(64);
await assert.rejects(provider.verify({
name: "RSASSA-PKCS1-v1_5",
}, keys.publicKey, signature, new ArrayBuffer(32)), {
message: "Unrecognized name",
});
});
});
});
context("X25519", () => {
class TestX25519Provider extends X25519Provider {
public override async onGenerateKey(algorithm: Algorithm, extractable: boolean, keyUsages: KeyUsage[], ..._args: any[]): Promise<CryptoKeyPair> {
const privateKey = new CryptoKey();
privateKey.algorithm = { name: "X25519" };
privateKey.type = "private";
privateKey.extractable = extractable;
privateKey.usages = ["deriveKey", "deriveBits"];

const publicKey = new CryptoKey();
publicKey.algorithm = { name: "X25519" };
publicKey.type = "public";
publicKey.extractable = true;
publicKey.usages = [];

return {
privateKey,
publicKey,
};
}

public async onDeriveKey(algorithm: Algorithm, baseKey: CryptoKey, derivedKeyType: Algorithm, extractable: boolean, keyUsages: KeyUsage[]): Promise<CryptoKey> {
return new CryptoKey();
}
public async onDeriveBits(algorithm: Algorithm, baseKey: CryptoKey, length: number): Promise<ArrayBuffer> {
return new ArrayBuffer(32);
}
}

const provider = new TestX25519Provider();

context("generateKey", () => {
it("should generate key pair", async () => {
const keys = await provider.generateKey({
name: "X25519",
}, true, ["deriveKey", "deriveBits"]);
assert.ok("privateKey" in keys);
assert.ok("publicKey" in keys);
assert.strictEqual(keys.privateKey.algorithm.name, "X25519");
assert.strictEqual(keys.privateKey.type, "private");
assert.strictEqual(keys.privateKey.extractable, true);
assert.deepStrictEqual(keys.privateKey.usages, ["deriveKey", "deriveBits"]);
assert.strictEqual(keys.publicKey.algorithm.name, "X25519");
assert.strictEqual(keys.publicKey.type, "public");
assert.strictEqual(keys.publicKey.extractable, true);
assert.deepStrictEqual(keys.publicKey.usages, []);
});
it("should throw error when algorithm is not correct", async () => {
await assert.rejects(provider.generateKey({
name: "RSASSA-PKCS1-v1_5",
}, true, ["deriveKey", "deriveBits"]), {
message: "Unrecognized name",
});
});
it("should throw error when keyUsages is not correct", async () => {
await assert.rejects(provider.generateKey({
name: "X25519",
}, true, ["encrypt", "decrypt"]), {
message: "Cannot create a key using the specified key usages",
});
});
});
context("deriveBits", () => {
it("should derive bits", async () => {
const keys = await provider.generateKey({
name: "X25519",
}, true, ["deriveKey", "deriveBits"]);
assert.ok("privateKey" in keys);
const bits = await provider.deriveBits({
name: "X25519",
public: keys.publicKey,
} as EcdhKeyDeriveParams, keys.privateKey, 32);
});
it("should throw error when algorithm is not correct", async () => {
const keys = await provider.generateKey({
name: "X25519",
}, true, ["deriveKey", "deriveBits"]);
assert.ok("privateKey" in keys);
await assert.rejects(provider.deriveBits({
name: "RSASSA-PKCS1-v1_5",
} as EcdhKeyDeriveParams, keys.privateKey, 32), {
message: "Unrecognized name",
});
});
});
});
});
Loading

0 comments on commit d96e153

Please sign in to comment.