From f2959557ce0007735850e4e75cf7839dd4a4300a Mon Sep 17 00:00:00 2001 From: Plamen Hristov Date: Mon, 22 May 2023 01:27:15 +0400 Subject: [PATCH] Add documentation and Github Actions (#4) --- .github/workflows/pr.yml | 49 +++++++++ .github/workflows/release.yml | 30 ++++++ README.md | 164 ++++++++++++++++++++++++++++- package.json | 32 +++++- src/constants.ts | 92 +---------------- src/ecdsa.ts | 189 ++++++++++++++++++---------------- src/eddsa.ts | 17 +-- src/enums.ts | 91 ++++++++++++++++ src/index.ts | 1 + src/types.ts | 15 +++ test/ecdsa.spec.ts | 54 +++++----- 11 files changed, 512 insertions(+), 222 deletions(-) create mode 100644 .github/workflows/pr.yml create mode 100644 .github/workflows/release.yml create mode 100644 src/enums.ts diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..021531b --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,49 @@ +name: Pull Request Checks + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + node-version: [16.x, 18.x, 20.x] + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js ${{ matrix.node-version }} ${{ matrix.os }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Run build + run: npm run build + + - name: Coveralls Parallel + uses: coverallsapp/github-action@v2 + with: + flag-name: run-${{ join(matrix.*, '-') }} + parallel: true + + finish: + needs: build + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@v2 + with: + parallel-finished: true + carryforward: "run-1,run-2" \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c03f715 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,30 @@ +name: Release new package version + +on: + push: + tags: + - 'v*' + +jobs: + publish-npm: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: 16.x + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Publish + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/README.md b/README.md index 6c0089b..6b0fd91 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,164 @@ # @blocksoft/cryptomate -NodeJS crypto module wrapper for easier use + +Ergonomic, zero-dependency crypto module wrapper for ECDSA and EdDSA signatures. + +[![Pull Request Checks](https://github.com/PlamenHristov/cryptomate/actions/workflows/pr.yml/badge.svg?branch=main&event=release)](https://github.com/PlamenHristov/cryptomate/actions/workflows/pr.yml) +[![Coverage Status](https://coveralls.io/repos/github/PlamenHristov/cryptomate/badge.svg?branch=main)](https://coveralls.io/github/PlamenHristov/cryptomate?branch=main) +[![Known Vulnerabilities](https://snyk.io/test/github/PlamenHristov/cryptomate/badge.svg?targetFile=package.json)](https://snyk.io/test/github/PlamenHristov/cryptomate?targetFile=package.json) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +Key features of the module include: + +- Generation of ECDSA and EdDSA key pairs. +- Conversion between PEM (Privacy-Enhanced Mail) and DER (Distinguished Encoding Rules) formats. +- Extraction of public key from a given private key. +- Message signing and verification using ECDSA and EdDSA algorithms. + +## Installation + +To install `@blocksoft/cryptomate`, use the following npm command: + +```bash +npm i --save @blocksoft/cryptomate +``` + +## Quick Start + +Here is an example demonstrating the use of the ECDSA functionality provided by this module: + +```javascript +const {ECDSA, EC_CURVE, Key, SignatureEncoding} = require('@blocksoft/cryptomate'); + +// Generate an ECDSA key pair. +const ecdsa = ECDSA.withCurve(EC_CURVE.secp256k1).genKeyPair(); + +// Sign a message. +let message = "Hello, World!"; +let signature = ecdsa.sign(message, SignatureEncoding.HEX); + +// Verify the signature. +console.log(ecdsa.verify(message, signature)); // Outputs: true + +// Export keys in PEM format. +let privateKeyPEM = ecdsa.toPEM(Key.privateKey); +let publicKeyPEM = ecdsa.toPEM(Key.publicKey); + +// Import keys from PEM format. +let importedECDSA = ECDSA.withCurve(EC_CURVE.secp256k1).fromPEM(privateKeyPEM, Key.privateKey); + +``` + +## API Reference + +### ECDSA + +#### ECDSA.withCurve(curve) + +A factory method to construct an ECDSA object with a given elliptic curve. + +##### Parameters + +- `curve` - The elliptic curve to use. This can be one of the following values: + - `EC_CURVE.P_256` - NIST P-256 curve. + - `EC_CURVE.P_384` - NIST P-384 curve. + - `EC_CURVE.P_521` - NIST P-521 curve. + - `EC_CURVE.SECP256K1` - SECP256K1 curve. + - `EC_CURVE.SECP256R1` - SECP256R1 curve. + - `EC_CURVE.SECP384R1` - SECP384R1 curve. + - `EC_CURVE.SECP521R1` - SECP521R1 curve. + - ... + +##### Returns + +An ECDSA object with the given elliptic curve. + +#### ECDSA.genKeyPair() + +Generates a new ECDSA key pair. + +##### Returns + +An ECDSA object with a newly generated key pair. + +#### ECDSA.fromPEM(pem, keyType) + +Constructs an ECDSA object from a given PEM string. + +##### Parameters + +- `pem` - The PEM string to construct the ECDSA object from. +- `keyType` - The type of key to construct. This can be one of the following values: + - `Key.privateKey` - Private key. + - `Key.publicKey` - Public key. + +##### Returns + +An ECDSA object with the given PEM string. + +#### ECDSA.toPEM(keyType) + +Converts the ECDSA object to a PEM string. + +##### Parameters + +- `keyType` - The type of key to convert. This can be one of the following values: + - `Key.privateKey` - Private key. + - `Key.publicKey` - Public key. + +##### Returns + +A PEM string representing the ECDSA object. + +#### ECDSA.getPublicKey() + +Extracts the public key from the ECDSA object. + +##### Returns + +A Buffer object containing the public key. + +#### ECDSA.sign(message, encoding) + +Signs a message using the ECDSA object. + +##### Parameters + +- `message` - The message to sign. +- `encoding` - The encoding of the message. This can be one of the following values: + - `SignatureEncoding.HEX` - The message is encoded in hexadecimal format. + - `SignatureEncoding.BASE64` - The message is encoded in Base64 format. + - `SignatureEncoding.UTF8` - The message is encoded in UTF-8 format. + - ... + +##### Returns + +A Buffer object containing the signature. + +#### ECDSA.verify(message, signature, encoding) + +Verifies a signature using the ECDSA object. + +##### Parameters + +- `message` - The message to verify. +- `signature` - The signature to verify. +- `encoding` - The encoding of the message. This can be one of the following values: + - `SignatureEncoding.HEX` - The message is encoded in hexadecimal format. + - `SignatureEncoding.BASE64` - The message is encoded in Base64 format. + - `SignatureEncoding.UTF8` - The message is encoded in UTF-8 format. + - ... + +##### Returns + +A boolean value indicating whether the signature is valid. + +### EdDSA + +Please note that the EdDSA class shares similar method names and functionalities to the ECDSA class for key pair +generation, +signing and verifying messages, and importing/exporting keys in various formats. +Refer to the ECDSA API documentation provided above for further details. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/package.json b/package.json index 9154884..913bc7b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test": "test" }, "scripts": { - "test": "jest", + "test": "jest --coverage", "clean": "rm -rf ./dist", "build": "npm run clean && npm run build:esm && npm run build:cjs", "build:esm": "tsc -p ./configs/tsconfig.esm.json && mv dist/esm/index.js dist/esm/index.mjs", @@ -33,6 +33,10 @@ "type": "git", "url": "git+https://github.com/PlamenHristov/cryptomate.git" }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, "keywords": [ "nodejs", "crypto", @@ -58,10 +62,25 @@ "plugin:@typescript-eslint/recommended" ], "rules": { - "semi": ["error", "never"], - "indent": ["error", 2], - "quotes": ["error", "double"], - "no-multiple-empty-lines": [2, {"max": 99999, "maxEOF": 0}] + "semi": [ + "error", + "never" + ], + "indent": [ + "error", + 2 + ], + "quotes": [ + "error", + "double" + ], + "no-multiple-empty-lines": [ + 2, + { + "max": 99999, + "maxEOF": 0 + } + ] }, "env": { "browser": true, @@ -72,5 +91,8 @@ "license": "MIT", "bugs": { "url": "https://github.com/PlamenHristov/cryptomate/issues" + }, + "volta": { + "node": "20.2.0" } } diff --git a/src/constants.ts b/src/constants.ts index 9bc03e1..9110c70 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,95 +1,5 @@ +import { EC_CURVE, ED_CURVE, Key } from "./enums" export const BYTE_LENGTH_IN_HEX = 2 -export enum Key { - publicKey = "publicKey", - privateKey = "privateKey", -} -export enum ED_CURVE { - ed25519 = "ed25519", - ed488 = "ed488", -} - -export enum EC_CURVE { - SM2 = "SM2", - brainpoolP160r1 = "brainpoolP160r1", - brainpoolP160t1 = "brainpoolP160t1", - brainpoolP192r1 = "brainpoolP192r1", - brainpoolP192t1 = "brainpoolP192t1", - brainpoolP224r1 = "brainpoolP224r1", - brainpoolP224t1 = "brainpoolP224t1", - brainpoolP256r1 = "brainpoolP256r1", - brainpoolP256t1 = "brainpoolP256t1", - brainpoolP320r1 = "brainpoolP320r1", - brainpoolP320t1 = "brainpoolP320t1", - brainpoolP384r1 = "brainpoolP384r1", - brainpoolP384t1 = "brainpoolP384t1", - brainpoolP512r1 = "brainpoolP512r1", - brainpoolP512t1 = "brainpoolP512t1", - c2pnb163v1 = "c2pnb163v1", - c2pnb163v2 = "c2pnb163v2", - c2pnb163v3 = "c2pnb163v3", - c2pnb176v1 = "c2pnb176v1", - c2pnb208w1 = "c2pnb208w1", - c2pnb272w1 = "c2pnb272w1", - c2pnb304w1 = "c2pnb304w1", - c2pnb368w1 = "c2pnb368w1", - c2tnb191v1 = "c2tnb191v1", - c2tnb191v2 = "c2tnb191v2", - c2tnb191v3 = "c2tnb191v3", - c2tnb239v1 = "c2tnb239v1", - c2tnb239v2 = "c2tnb239v2", - c2tnb239v3 = "c2tnb239v3", - c2tnb359v1 = "c2tnb359v1", - c2tnb431r1 = "c2tnb431r1", - prime192v1 = "prime192v1", - prime192v2 = "prime192v2", - prime192v3 = "prime192v3", - prime239v1 = "prime239v1", - prime239v2 = "prime239v2", - prime239v3 = "prime239v3", - prime256v1 = "prime256v1", - secp112r1 = "secp112r1", - secp112r2 = "secp112r2", - secp128r1 = "secp128r1", - secp128r2 = "secp128r2", - secp160k1 = "secp160k1", - secp160r1 = "secp160r1", - secp160r2 = "secp160r2", - secp192k1 = "secp192k1", - secp224k1 = "secp224k1", - secp224r1 = "secp224r1", - secp256k1 = "secp256k1", - secp384r1 = "secp384r1", - secp521r1 = "secp521r1", - sect113r1 = "sect113r1", - sect113r2 = "sect113r2", - sect131r1 = "sect131r1", - sect131r2 = "sect131r2", - sect163k1 = "sect163k1", - sect163r1 = "sect163r1", - sect163r2 = "sect163r2", - sect193r1 = "sect193r1", - sect193r2 = "sect193r2", - sect233k1 = "sect233k1", - sect233r1 = "sect233r1", - sect239k1 = "sect239k1", - sect283k1 = "sect283k1", - sect283r1 = "sect283r1", - sect409k1 = "sect409k1", - sect409r1 = "sect409r1", - sect571k1 = "sect571k1", - sect571r1 = "sect571r1", - wap_wsg_idm_ecid_wtls1 = "wap-wsg-idm-ecid-wtls1", - wap_wsg_idm_ecid_wtls10 = "wap-wsg-idm-ecid-wtls10", - wap_wsg_idm_ecid_wtls11 = "wap-wsg-idm-ecid-wtls11", - wap_wsg_idm_ecid_wtls12 = "wap-wsg-idm-ecid-wtls12", - wap_wsg_idm_ecid_wtls3 = "wap-wsg-idm-ecid-wtls3", - wap_wsg_idm_ecid_wtls4 = "wap-wsg-idm-ecid-wtls4", - wap_wsg_idm_ecid_wtls5 = "wap-wsg-idm-ecid-wtls5", - wap_wsg_idm_ecid_wtls6 = "wap-wsg-idm-ecid-wtls6", - wap_wsg_idm_ecid_wtls7 = "wap-wsg-idm-ecid-wtls7", - wap_wsg_idm_ecid_wtls8 = "wap-wsg-idm-ecid-wtls8", - wap_wsg_idm_ecid_wtls9 = "wap-wsg-idm-ecid-wtls9", -} export const EC_CURVE_TO_OID: Record = { [EC_CURVE.SM2]: "06082a811ccf5501822d", diff --git a/src/ecdsa.ts b/src/ecdsa.ts index b5a6882..402dc69 100644 --- a/src/ecdsa.ts +++ b/src/ecdsa.ts @@ -1,9 +1,18 @@ import * as crypto from "crypto" -import {BYTE_LENGTH_IN_HEX, EC_CURVE, EC_CURVE_TO_OID, Key} from "./constants" -import {ISigner, SignatureEncoding, SignatureResponse, SignatureType} from "./types" - -export class ECDSA implements ISigner { +import {BYTE_LENGTH_IN_HEX, EC_CURVE_TO_OID} from "./constants" +import {EC_CURVE, Key} from "./enums" +import { + EncodingResponse, + IKey, + ISigner, + KeyEncoding, + SignatureEncoding, + SignatureResponse, + SignatureType +} from "./types" + +export class ECDSA implements ISigner, IKey { private readonly EC_PUBLIC_KEY_OID = "06072a8648ce3d0201" private readonly ECDSA_OID_PREFIX = "02010030" private readonly ECDSA_OID_SUFFIX = "020101" @@ -31,10 +40,7 @@ export class ECDSA implements ISigner { } public get privateKey(): string { - const pkcs8Hex = this._privateKey.export({ - format: "der", - type: "pkcs8", - }).toString("hex") + const pkcs8Hex = this.export("der", Key.privateKey).toString("hex") const privateKeyLengthSizeIndex = pkcs8Hex.indexOf(this.ECDSA_OID_SUFFIX) + this.ECDSA_OID_SUFFIX.length + 2 const privateKeyLengthSizeIndexEnd = privateKeyLengthSizeIndex + 2 const privateKeySize = pkcs8Hex.substring(privateKeyLengthSizeIndex, privateKeyLengthSizeIndexEnd) @@ -43,8 +49,8 @@ export class ECDSA implements ISigner { } public get publicKey(): string { - const pkcs8Hex = this.export("der", Key.publicKey).toString("hex") - const pkLengthIndexStart = pkcs8Hex.indexOf(this.oid) + this.oid.length + BYTE_LENGTH_IN_HEX + const pkcs8Hex = this._export("der", Key.publicKey).toString("hex") + const pkLengthIndexStart = pkcs8Hex.lastIndexOf(this.oid) + this.oid.length + BYTE_LENGTH_IN_HEX let pkLengthIndexEnd = pkLengthIndexStart while (pkcs8Hex.substring(pkLengthIndexEnd, pkLengthIndexEnd + BYTE_LENGTH_IN_HEX) != this.PUBLIC_KEY_START_INDICATOR) { @@ -52,46 +58,59 @@ export class ECDSA implements ISigner { } const publicKeySize = pkcs8Hex.substring(pkLengthIndexStart, pkLengthIndexEnd) - const publicKeyStart= pkLengthIndexEnd + this.PUBLIC_KEY_START_INDICATOR.length + const publicKeyStart = pkLengthIndexEnd + this.PUBLIC_KEY_START_INDICATOR.length const publicKeyEnd = publicKeyStart + (this._decodeOidLength(publicKeySize) * BYTE_LENGTH_IN_HEX) return pkcs8Hex.substring(publicKeyStart, publicKeyEnd) } - private export(format: crypto.KeyFormat, key: Key = Key.privateKey): Buffer { + private _export(format: crypto.KeyFormat, key: Key = Key.privateKey): Buffer { if (key == Key.privateKey) { return this._privateKey.export({ format: format as any, type: "pkcs8", }) as Buffer } + return this._publicKey.export({ format: format as any, type: "spki", }) as Buffer } - private import(keyData: string | Buffer, format: crypto.KeyFormat, key: Key) { + private export(format: T, key: Key = Key.privateKey): EncodingResponse[T] { + // There is a bug in nodejs >16.x where importing the public key directly the DER encoding + // concatenates the OID twice instead of adding the public key prefix + oid. + if (!this._privateKey) { + if (format === "der") + return this._encodeDER(this.publicKey, key) as EncodingResponse[T] + + return this._encodePEM(this._encodeDER(this.publicKey, key), key) as EncodingResponse[T] + } + + return this._export(format, key) as EncodingResponse[T] + } + + private import(keyData: string | Buffer, format: "pem", key: Key) { this.checkPrivateKeyNotAlreadyImported() if (key == Key.privateKey) { this._privateKey = crypto.createPrivateKey({ key: keyData, format, - type: "pkcs8", }) this._publicKey = crypto.createPublicKey(this._privateKey) } else { this._publicKey = crypto.createPublicKey({ key: keyData, format, - type: "spki", }) } } - public fromDER(der: string, key: Key = Key.privateKey): ECDSA { - this.import(Buffer.from(der, "hex"), "der", key) + public fromDER(der: string | Buffer, key: Key = Key.privateKey): ECDSA { + const derFormat = Buffer.isBuffer(der) ? der : Buffer.from(der, "hex") + this.import(this._encodePEM(derFormat, key), "pem", key) return this } @@ -101,62 +120,88 @@ export class ECDSA implements ISigner { } public toDER(key: Key = Key.privateKey): Buffer { - this.validateKeyExists(key) - if (key == Key.publicKey) - return this._publicKey.export({ - format: "der", - type: "spki", - }) - - return this._privateKey.export({ - format: "der", - type: "pkcs8", - }) + this._validateKeyExists(key) + return this.export("der", key) } public toPEM(key: Key = Key.privateKey): string { - this.validateKeyExists(key) - - return this._encodePEM(this.toDER(key).toString("base64"), key) + this._validateKeyExists(key) + return this.export("pem", key) } - sign(msg: string, enc: T): SignatureResponse[T] { - this.validateKeyExists(Key.privateKey) + sign(msg: string, enc?: T): SignatureResponse[T] { + this._validateKeyExists(Key.privateKey) const signature = crypto.sign( null, Buffer.isBuffer(msg) ? msg : Buffer.from(msg, "hex"), { - key: this._privateKey, + key: this.toPEM(Key.privateKey), dsaEncoding: "ieee-p1363" } ) if (enc === "hex") return signature.toString("hex") as SignatureResponse[T] if (enc === "buffer") return signature as SignatureResponse[T] - if (enc === "object") - return { - r: signature.subarray(0, signature.length / 2).toString("hex"), - s: signature.subarray(signature.length / 2, signature.length).toString("hex") - } as SignatureResponse[T] - throw new Error(`Unsupported encoding: ${enc}`) + return { + r: signature.subarray(0, signature.length / 2).toString("hex"), + s: signature.subarray(signature.length / 2, signature.length).toString("hex") + } as SignatureResponse[T] } verify(msg: string, signature: SignatureType): boolean { - this.validateKeyExists(Key.publicKey) + this._validateKeyExists(Key.publicKey) const castedSignature = this._castSignature(signature) return crypto.verify( null, Buffer.from(msg, "hex"), { - key: this._publicKey, + key: this.toPEM(Key.publicKey), dsaEncoding: "ieee-p1363" }, castedSignature ) } + keyFromPublic(publicKey: string | Buffer, enc: crypto.BinaryToTextEncoding = "hex"): ECDSA { + if (this._privateKey) throw new Error("Cannot import public key when private key is set") + + const serializedKey = Buffer.isBuffer(publicKey) ? publicKey : Buffer.from(publicKey, enc) + this.import(this._encodePEM(this._encodeDER(serializedKey.toString("hex"), Key.publicKey), Key.publicKey), "pem", Key.publicKey) + return this + } + + genKeyPair(): ECDSA { + const keypair = crypto.generateKeyPairSync("ec", { + namedCurve: this.curve, + }) + this._privateKey = keypair.privateKey + this._publicKey = keypair.publicKey + return this + } + + keyFromPrivate(privateKey: string | Buffer, enc: crypto.BinaryToTextEncoding = "hex"): ECDSA { + const serializedKey = Buffer.isBuffer(privateKey) ? privateKey : Buffer.from(privateKey, enc) + + this.ecdh.setPrivateKey(serializedKey) + + const publicKey = this.ecdh.getPublicKey() + const pkPEM = this._encodePEM(this._encodeDER(publicKey.toString("hex"), Key.publicKey), Key.publicKey) + this._publicKey = crypto.createPublicKey({ + key: pkPEM, + format: "pem", + }) + + const skPEM = this._encodePEM(this._encodeDER(privateKey.toString("hex"), Key.privateKey), Key.privateKey) + this._privateKey = crypto.createPrivateKey({ + key: skPEM, + format: "pem", + }) + + return this + } + private _castSignature(signature: SignatureType): Buffer { if (Buffer.isBuffer(signature)) return signature @@ -168,18 +213,25 @@ export class ECDSA implements ISigner { return Buffer.from(signature, "hex") } - private validateKeyExists(key: Key) { + private _validateKeyExists(key: Key) { if (key == Key.privateKey && !this._privateKey) throw new Error("No private key set") if (key == Key.publicKey && !this._publicKey) throw new Error("No public key set") } - private _encodePEM(keyDer: string, key: Key): string { + private _encodePEM(keyDer: Buffer, key: Key): string { + let b64Der = keyDer.toString("base64") + let encodedPEM = "" + while (b64Der.length) { + encodedPEM += b64Der.substring(0, 64) + "\n" + b64Der = b64Der.substring(64) + } + if (key == Key.privateKey) - return `-----BEGIN PRIVATE KEY-----\n${keyDer}\n-----END PRIVATE KEY-----` + return `-----BEGIN PRIVATE KEY-----\n${encodedPEM}-----END PRIVATE KEY-----\n` - return `-----BEGIN PUBLIC KEY-----\n${keyDer}\n-----END PUBLIC KEY-----` + return `-----BEGIN PUBLIC KEY-----\n${encodedPEM}-----END PUBLIC KEY-----\n` } private _encodeDER(hex: string, key: Key): Buffer { @@ -190,7 +242,7 @@ export class ECDSA implements ISigner { private _derEncodePublicKey(publicKeyHex: string): Buffer { const paddedPublicKeyHex = `${this.PUBLIC_KEY_START_INDICATOR}${publicKeyHex}` - const encodedPublicKey = `03${this._encodeOidLength(paddedPublicKeyHex)}${paddedPublicKeyHex}` + const encodedPublicKey = `03${this._encodeOidLength(paddedPublicKeyHex)}${paddedPublicKeyHex}` const keyMetadata = `${this.EC_PUBLIC_KEY_OID}${this.oid}` const algorithmIdentifier = `30${this._encodeOidLength(keyMetadata)}${keyMetadata}` const fullString = `${algorithmIdentifier}${encodedPublicKey}` @@ -201,49 +253,6 @@ export class ECDSA implements ISigner { if (this._privateKey) throw new Error("Private key already imported") } - keyFromPublic(publicKey: string | Buffer, enc: crypto.BinaryToTextEncoding = "hex"): ECDSA { - if (this._privateKey) throw new Error("Cannot import public key when private key is set") - - const serializedKey = Buffer.isBuffer(publicKey) ? publicKey : Buffer.from(publicKey, enc) - this._publicKey = crypto.createPublicKey({ - key: this._encodeDER(serializedKey.toString("hex"), Key.publicKey), - format: "der", - type: "spki", - }) - return this - } - - genKeyPair(): ECDSA { - const keypair = crypto.generateKeyPairSync("ec", { - namedCurve: this.curve - }) - this._privateKey = keypair.privateKey - this._publicKey = keypair.publicKey - return this - } - - keyFromPrivate(privateKey: string | Buffer, enc: crypto.BinaryToTextEncoding = "hex"): ECDSA { - const serializedKey = Buffer.isBuffer(privateKey) ? privateKey : Buffer.from(privateKey, enc) - - this.ecdh.setPrivateKey(serializedKey) - - const publicKey = this.ecdh.getPublicKey() - this._publicKey = crypto.createPublicKey({ - key: this._encodeDER(publicKey.toString("hex"), Key.publicKey), - format: "der", - type: "spki", - }) - - const derPrivateKey = this._derEncodePrivateKey(serializedKey.toString("hex")) - this._privateKey = crypto.createPrivateKey({ - key: derPrivateKey, - format: "der", - type: "pkcs8", - }) - - return this - } - private _decodeOidLength(hexString: string): number { const firstByte = parseInt(hexString.slice(0, BYTE_LENGTH_IN_HEX), 16) diff --git a/src/eddsa.ts b/src/eddsa.ts index 5a5ebf7..1bb1ff9 100644 --- a/src/eddsa.ts +++ b/src/eddsa.ts @@ -1,8 +1,9 @@ import * as crypto from "crypto" -import { ED_CURVE, ED_CURVE_TO_DER_MARKER, Key } from "./constants" -import {ISigner, SignatureEncoding, SignatureResponse, SignatureType} from "./types" +import { ED_CURVE_TO_DER_MARKER } from "./constants" +import { ED_CURVE, Key } from "./enums" +import { IKey, ISigner, SignatureEncoding, SignatureResponse, SignatureType } from "./types" -export class EdDSA implements ISigner { +export class EdDSA implements ISigner, IKey { private curve: ED_CURVE private _privateKey: crypto.KeyObject private _publicKey: crypto.KeyObject @@ -69,7 +70,7 @@ export class EdDSA implements ISigner { } - public fromDER(der: string|Buffer, key: Key = Key.privateKey): EdDSA { + public fromDER(der: string | Buffer, key: Key = Key.privateKey): EdDSA { this.import(Buffer.isBuffer(der) ? der : Buffer.from(der, "hex"),"der", key) return this } @@ -91,7 +92,7 @@ export class EdDSA implements ISigner { return this._encodePEM(this.toDER(key), key) } - sign(msg: string | Buffer, enc: T): SignatureResponse[T] { + sign(msg: string | Buffer, enc?: T): SignatureResponse[T] { this.validateKeyExists(Key.privateKey) const signature = crypto.sign( @@ -124,7 +125,7 @@ export class EdDSA implements ISigner { ) } - keyFromPrivate(privateKey: string | Buffer, enc: crypto.BinaryToTextEncoding = "hex"): EdDSA { + public keyFromPrivate(privateKey: string | Buffer, enc: crypto.BinaryToTextEncoding = "hex"): EdDSA { const serializedKey = Buffer.isBuffer(privateKey) ? privateKey : Buffer.from(privateKey, enc) this._privateKey = crypto.createPrivateKey({ key: this._encodeDER(serializedKey.toString("hex"), Key.privateKey), @@ -135,7 +136,7 @@ export class EdDSA implements ISigner { return this } - keyFromPublic(publicKey: string | Buffer, enc: crypto.BinaryToTextEncoding = "hex"): EdDSA { + public keyFromPublic(publicKey: string | Buffer, enc: crypto.BinaryToTextEncoding = "hex"): EdDSA { if (this._privateKey) throw new Error("Cannot import public key when private key is set") const serializedKey = Buffer.isBuffer(publicKey) ? publicKey : Buffer.from(publicKey, enc) @@ -147,7 +148,7 @@ export class EdDSA implements ISigner { return this } - genKeyPair(): EdDSA { + public genKeyPair(): EdDSA { const keypair = crypto.generateKeyPairSync(this.curve as any) this._privateKey = keypair.privateKey this._publicKey = keypair.publicKey diff --git a/src/enums.ts b/src/enums.ts new file mode 100644 index 0000000..c52071c --- /dev/null +++ b/src/enums.ts @@ -0,0 +1,91 @@ +export enum Key { + publicKey = "publicKey", + privateKey = "privateKey", +} +export enum ED_CURVE { + ed25519 = "ed25519", + ed488 = "ed488", +} + +export enum EC_CURVE { + SM2 = "SM2", + brainpoolP160r1 = "brainpoolP160r1", + brainpoolP160t1 = "brainpoolP160t1", + brainpoolP192r1 = "brainpoolP192r1", + brainpoolP192t1 = "brainpoolP192t1", + brainpoolP224r1 = "brainpoolP224r1", + brainpoolP224t1 = "brainpoolP224t1", + brainpoolP256r1 = "brainpoolP256r1", + brainpoolP256t1 = "brainpoolP256t1", + brainpoolP320r1 = "brainpoolP320r1", + brainpoolP320t1 = "brainpoolP320t1", + brainpoolP384r1 = "brainpoolP384r1", + brainpoolP384t1 = "brainpoolP384t1", + brainpoolP512r1 = "brainpoolP512r1", + brainpoolP512t1 = "brainpoolP512t1", + c2pnb163v1 = "c2pnb163v1", + c2pnb163v2 = "c2pnb163v2", + c2pnb163v3 = "c2pnb163v3", + c2pnb176v1 = "c2pnb176v1", + c2pnb208w1 = "c2pnb208w1", + c2pnb272w1 = "c2pnb272w1", + c2pnb304w1 = "c2pnb304w1", + c2pnb368w1 = "c2pnb368w1", + c2tnb191v1 = "c2tnb191v1", + c2tnb191v2 = "c2tnb191v2", + c2tnb191v3 = "c2tnb191v3", + c2tnb239v1 = "c2tnb239v1", + c2tnb239v2 = "c2tnb239v2", + c2tnb239v3 = "c2tnb239v3", + c2tnb359v1 = "c2tnb359v1", + c2tnb431r1 = "c2tnb431r1", + prime192v1 = "prime192v1", + prime192v2 = "prime192v2", + prime192v3 = "prime192v3", + prime239v1 = "prime239v1", + prime239v2 = "prime239v2", + prime239v3 = "prime239v3", + prime256v1 = "prime256v1", + secp112r1 = "secp112r1", + secp112r2 = "secp112r2", + secp128r1 = "secp128r1", + secp128r2 = "secp128r2", + secp160k1 = "secp160k1", + secp160r1 = "secp160r1", + secp160r2 = "secp160r2", + secp192k1 = "secp192k1", + secp224k1 = "secp224k1", + secp224r1 = "secp224r1", + secp256k1 = "secp256k1", + secp384r1 = "secp384r1", + secp521r1 = "secp521r1", + sect113r1 = "sect113r1", + sect113r2 = "sect113r2", + sect131r1 = "sect131r1", + sect131r2 = "sect131r2", + sect163k1 = "sect163k1", + sect163r1 = "sect163r1", + sect163r2 = "sect163r2", + sect193r1 = "sect193r1", + sect193r2 = "sect193r2", + sect233k1 = "sect233k1", + sect233r1 = "sect233r1", + sect239k1 = "sect239k1", + sect283k1 = "sect283k1", + sect283r1 = "sect283r1", + sect409k1 = "sect409k1", + sect409r1 = "sect409r1", + sect571k1 = "sect571k1", + sect571r1 = "sect571r1", + wap_wsg_idm_ecid_wtls1 = "wap-wsg-idm-ecid-wtls1", + wap_wsg_idm_ecid_wtls10 = "wap-wsg-idm-ecid-wtls10", + wap_wsg_idm_ecid_wtls11 = "wap-wsg-idm-ecid-wtls11", + wap_wsg_idm_ecid_wtls12 = "wap-wsg-idm-ecid-wtls12", + wap_wsg_idm_ecid_wtls3 = "wap-wsg-idm-ecid-wtls3", + wap_wsg_idm_ecid_wtls4 = "wap-wsg-idm-ecid-wtls4", + wap_wsg_idm_ecid_wtls5 = "wap-wsg-idm-ecid-wtls5", + wap_wsg_idm_ecid_wtls6 = "wap-wsg-idm-ecid-wtls6", + wap_wsg_idm_ecid_wtls7 = "wap-wsg-idm-ecid-wtls7", + wap_wsg_idm_ecid_wtls8 = "wap-wsg-idm-ecid-wtls8", + wap_wsg_idm_ecid_wtls9 = "wap-wsg-idm-ecid-wtls9", +} diff --git a/src/index.ts b/src/index.ts index bbeb2ca..feceade 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export * from "./ecdsa" export * from "./eddsa" export * from "./constants" +export * from "./enums" export * from "./types" diff --git a/src/types.ts b/src/types.ts index 2b2b7de..ed72d4c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,9 +1,24 @@ +import { Key } from "./enums" +import { BinaryToTextEncoding } from "crypto" + export type SignatureEncoding = "hex" | "buffer" | "object"; export type SignatureObject = { r: string; s: string }; export type SignatureType = string | Buffer | SignatureObject; export type SignatureResponse = { "hex": string; "buffer": Buffer; "object": SignatureObject; }; +export type KeyEncoding = "pem" | "der"; +export type EncodingResponse = { "der": Buffer, "pem": string } export interface ISigner { sign: (msg: string, enc: T) => SignatureResponse[T]; verify: (msg: string, signature: SignatureType) => boolean; } + +export interface IKey { + toDER: (key: Key) => Buffer; + fromDER: (data : string | Buffer, key: Key) => T; + toPEM: (key: Key) => string; + fromPEM: (data: string, key: Key) => T; + keyFromPrivate(privateKey: string | Buffer, enc: BinaryToTextEncoding): T; + keyFromPublic(publicKey: string | Buffer, enc: BinaryToTextEncoding): T ; + genKeyPair(): T; +} \ No newline at end of file diff --git a/test/ecdsa.spec.ts b/test/ecdsa.spec.ts index 251b02b..c46fa23 100644 --- a/test/ecdsa.spec.ts +++ b/test/ecdsa.spec.ts @@ -1,5 +1,5 @@ -import {ECDSA, Key, EC_CURVE} from "../src" import * as crypto from "crypto" +import {ECDSA, Key, EC_CURVE} from "../src" describe("ECDSA", () => { @@ -29,6 +29,7 @@ describe("ECDSA", () => { test(`${curve} exports private key to DER format`, () => { ecdsa.genKeyPair() + const privateKeyDER = ecdsa.toDER(Key.privateKey) expect(privateKeyDER).toBeDefined() @@ -59,78 +60,77 @@ describe("ECDSA", () => { expect(ecdsa.verify(message, signature)).toBeTruthy() }) + test(`${curve} verify works with public key only`, () => { + ecdsa.genKeyPair() + + const message = "test message" + const signature = ecdsa.sign(message) + const ecdsa2 = ECDSA.withCurve(curve).keyFromPublic(ecdsa.publicKey) + + expect(ecdsa2.verify(message, signature)).toBeTruthy() + }) + + test(`${curve} throws error when private key is not set for signing`, () => { + const ecdsa = new ECDSA(curve) const message = "test message" expect(() => ecdsa.sign(message)).toThrow("No private key set") }) test(`${curve} throws error when public key is not set for verification`, () => { + const ecdsa = new ECDSA(curve) const message = "test message" const signature = "test signature" expect(() => ecdsa.verify(message, signature)).toThrow("No public key set") }) - test(`${curve} converts correctly from PEM to DER and back to PEM for private key`, () => { ecdsa.genKeyPair() - const originalPrivateKeyPEM = ecdsa.toPEM(Key.privateKey) - const privateKeyDER = ecdsa.toDER(Key.privateKey) - const ecdsa2 = ECDSA.withCurve(curve) - ecdsa2.fromDER(privateKeyDER, Key.privateKey) - const convertedPrivateKeyPEM = ecdsa2.toPEM(Key.privateKey) + const originalPrivateKeyPEM = ecdsa.toPEM(Key.privateKey) + const ecdsa2 = ECDSA.withCurve(curve).fromDER(ecdsa.toDER(Key.privateKey), Key.privateKey) - expect(convertedPrivateKeyPEM).toEqual(originalPrivateKeyPEM) + expect(ecdsa2.toPEM(Key.privateKey)).toEqual(originalPrivateKeyPEM) }) test(`${curve} converts correctly from DER to PEM and back to DER for private key`, () => { ecdsa.genKeyPair() - const originalPrivateKeyDER = ecdsa.toDER(Key.privateKey) - const privateKeyPEM = ecdsa.toPEM(Key.privateKey) - const ecdsa2 = ECDSA.withCurve(curve) - ecdsa2.fromPEM(privateKeyPEM, Key.privateKey) - const convertedPrivateKeyDER = ecdsa2.toDER(Key.privateKey) + const ecdsa2 = ECDSA.withCurve(curve).fromPEM(ecdsa.toPEM(Key.privateKey), Key.privateKey) - expect(convertedPrivateKeyDER).toEqual(originalPrivateKeyDER) + expect(ecdsa2.toDER(Key.privateKey)).toEqual(ecdsa.toDER(Key.privateKey)) }) test(`${curve} converts correctly from PEM to DER and back to PEM for public key`, () => { ecdsa.genKeyPair() - const originalPublicKeyPEM = ecdsa.toPEM(Key.publicKey) - const publicKeyDER = ecdsa.toDER(Key.publicKey) - const ecdsa2 = ECDSA.withCurve(curve) - ecdsa2.fromDER(publicKeyDER, Key.publicKey) - const convertedPublicKeyPEM = ecdsa2.toPEM(Key.publicKey) + const ecdsa2 = ECDSA.withCurve(curve).fromDER(ecdsa.toDER(Key.publicKey), Key.publicKey) - expect(convertedPublicKeyPEM).toEqual(originalPublicKeyPEM) + expect(ecdsa2.toPEM(Key.publicKey)).toEqual(ecdsa.toPEM(Key.publicKey)) }) test(`${curve} converts correctly from DER to PEM and back to DER for public key`, () => { ecdsa.genKeyPair() - const originalPublicKeyDER = ecdsa.toDER(Key.publicKey) - const publicKeyPEM = ecdsa.toPEM(Key.publicKey) - const ecdsa2 = ECDSA.withCurve(curve) - ecdsa2.fromPEM(publicKeyPEM, Key.publicKey) - const convertedPublicKeyDER = ecdsa2.toDER(Key.publicKey) + const ecdsa2 = ECDSA.withCurve(curve).fromPEM(ecdsa.toPEM(Key.publicKey), Key.publicKey) - expect(convertedPublicKeyDER).toEqual(originalPublicKeyDER) + expect(ecdsa2.toDER(Key.publicKey).toString("hex")).toEqual(ecdsa.toDER(Key.publicKey).toString("hex")) }) test(`${curve} correctly imports hex encoded private key`, () => { ecdsa.genKeyPair() - const importedECDSA = ECDSA.withCurve(curve).keyFromPrivate(ecdsa.privateKey) + expect(importedECDSA.privateKey).toEqual(ecdsa.privateKey) }) test(`${curve} correctly imports hex encoded public key`, () => { ecdsa.genKeyPair() + const importedECDSA = ECDSA.withCurve(curve).keyFromPublic(ecdsa.publicKey) + expect(importedECDSA.publicKey).toEqual(ecdsa.publicKey) }) })