Skip to content

Commit

Permalink
Merge pull request #75 from PeculiarVentures:ed25519
Browse files Browse the repository at this point in the history
Fix ES Modules for React Native and Add Ed25519/X25519 Support
  • Loading branch information
microshine authored May 28, 2024
2 parents b26d67e + a0cf289 commit a0d2821
Show file tree
Hide file tree
Showing 14 changed files with 715 additions and 207 deletions.
17 changes: 9 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
],
"main": "build/webcrypto.js",
"module": "build/webcrypto.es.js",
"react-native": "build/webcrypto.es.js",
"types": "index.d.ts",
"scripts": {
"test": "mocha",
Expand Down Expand Up @@ -55,24 +56,24 @@
"devDependencies": {
"@peculiar/webcrypto-test": "^1.0.7",
"@types/mocha": "^10.0.6",
"@types/node": "^20.11.30",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
"@types/node": "^20.12.12",
"@typescript-eslint/eslint-plugin": "^7.10.0",
"@typescript-eslint/parser": "^7.10.0",
"eslint": "^8.57.0",
"eslint-plugin-import": "^2.29.1",
"mocha": "^10.4.0",
"rimraf": "^5.0.5",
"rollup": "^4.13.2",
"rimraf": "^5.0.7",
"rollup": "^4.17.2",
"rollup-plugin-typescript2": "^0.36.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.3"
"typescript": "^5.4.5"
},
"dependencies": {
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/json-schema": "^1.1.12",
"pvtsutils": "^1.3.5",
"tslib": "^2.6.2",
"webcrypto-core": "^1.7.9"
"webcrypto-core": "^1.8.0"
},
"nyc": {
"extension": [
Expand Down Expand Up @@ -101,4 +102,4 @@
],
"spec": "test/**/*.ts"
}
}
}
2 changes: 1 addition & 1 deletion src/mechs/aes/aes_cmac.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Buffer } from "buffer";
import * as crypto from "crypto";
import crypto from "crypto";
import * as core from "webcrypto-core";
import { AesCrypto } from "./crypto";
import { AesCryptoKey } from "./key";
Expand Down
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
2 changes: 1 addition & 1 deletion src/mechs/rsa/rsa_es.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as crypto from "crypto";
import crypto from "crypto";
import { Convert } from "pvtsutils";
import * as core from "webcrypto-core";
import { RsaCrypto } from "./crypto";
Expand Down
6 changes: 5 additions & 1 deletion src/subtle.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as crypto from "crypto";
import crypto from "crypto";
import * as process from "process";
import * as core from "webcrypto-core";
import {
Expand All @@ -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 a0d2821

Please sign in to comment.