From b6ee108816d87b7f406885a2585eea03e68ed4e4 Mon Sep 17 00:00:00 2001 From: Weiliang Li Date: Mon, 4 Sep 2023 00:13:25 +0900 Subject: [PATCH] Support curve25519 tentatively (#732) --- .github/workflows/cd.yml | 2 +- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 5 +- README.md | 57 +++++++++++---- package.json | 4 +- pnpm-lock.yaml | 126 +++++++++++++++++----------------- src/config.ts | 37 +++++++--- src/consts.ts | 7 +- src/index.ts | 1 + src/keys/PrivateKey.ts | 19 +++-- src/keys/PublicKey.ts | 41 ++++++----- src/keys/index.ts | 4 +- src/utils/compat.ts | 7 +- src/utils/elliptic.ts | 92 +++++++++++++++++++++---- tests/crypt.test.ts | 101 +++++++++++++++++++-------- tests/keys.test.ts | 2 +- tests/utils/compat.test.ts | 17 ++--- tests/utils/elliptic.test.ts | 49 ++++++++++++- tests/utils/symmetric.test.ts | 60 ++++++++-------- 19 files changed, 427 insertions(+), 206 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 5810e28..64b53f2 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,4 +1,4 @@ -name: Publish +name: CD on: release: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b7a508..4ee30c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Build +name: CI on: push: diff --git a/CHANGELOG.md b/CHANGELOG.md index 95a1c26..029667f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.4.1 ~ 0.4.5 +- Support curve25519 (x25519 and ed25519) tentatively - Revamp browser compatibility - Export config - Fix symmetric encryption internal types @@ -12,8 +13,8 @@ ## 0.4.0 -- Change secp256k1 library to [noble-curves](https://github.com/paulmillr/noble-curves), which is [audited](https://github.com/paulmillr/noble-curves/tree/main/audit) -- Change hash library to [noble-hashes](https://github.com/paulmillr/noble-hashes) +- Change secp256k1 library to audited [noble-curves](https://github.com/paulmillr/noble-curves) +- Change hash library to audited [noble-hashes](https://github.com/paulmillr/noble-hashes) - Change test library to [jest](https://jestjs.io/) - Bump dependencies - Drop Node 14 support diff --git a/README.md b/README.md index 1e636f5..3b5b2eb 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ [![CI](https://img.shields.io/github/actions/workflow/status/ecies/js/ci.yml)](https://github.com/ecies/js/actions) [![Codecov](https://img.shields.io/codecov/c/github/ecies/js.svg)](https://codecov.io/gh/ecies/js) -Elliptic Curve Integrated Encryption Scheme for secp256k1 in TypeScript. +Elliptic Curve Integrated Encryption Scheme for secp256k1/curve25519 in TypeScript. -This is the JavaScript/TypeScript version of [eciespy](https://github.com/ecies/py) with a built-in class-like secp256k1 [API](#privatekey), you may go there for detailed documentation and learn the mechanism under the hood. +This is the JavaScript/TypeScript version of [eciespy](https://github.com/ecies/py) with a built-in class-like secp256k1/curve25519 [API](#privatekey), you may go there for detailed documentation and learn the mechanism under the hood. If you want a WASM version to run directly in modern browsers or on some blockchains, check [`ecies-wasm`](https://github.com/ecies/rs-wasm). @@ -30,13 +30,15 @@ Run the code below with `npx ts-node`. 'hello world🌍' ``` +See [Configuration](#configuration) to control with more granularity. + ## API ### `encrypt(receiverRawPK: string | Uint8Array, msg: Uint8Array): Buffer` Parameters: -- **receiverRawPK** - Receiver's secp256k1 public key, hex string or buffer +- **receiverRawPK** - Receiver's public key, hex string or buffer - **msg** - Data to encrypt Returns: **Buffer** @@ -45,7 +47,7 @@ Returns: **Buffer** Parameters: -- **receiverRawSK** - Receiver's secp256k1 private key, hex string or buffer +- **receiverRawSK** - Receiver's private key, hex string or buffer - **msg** - Data to decrypt Returns: **Buffer** @@ -66,8 +68,9 @@ equals(other: PrivateKey): boolean; - Properties ```typescript -readonly secret: Buffer; +get secret(): Buffer; readonly publicKey: PublicKey; +private readonly data; ``` ### `PublicKey` @@ -76,7 +79,7 @@ readonly publicKey: PublicKey; ```typescript static fromHex(hex: string): PublicKey; -constructor(buffer: Uint8Array); +constructor(data: Uint8Array); toHex(compressed?: boolean): string; decapsulate(sk: PrivateKey): Uint8Array; equals(other: PublicKey): boolean; @@ -85,19 +88,30 @@ equals(other: PublicKey): boolean; - Properties ```typescript -readonly uncompressed: Buffer; -readonly compressed: Buffer; +get uncompressed(): Buffer; +get compressed(): Buffer; +private readonly data; ``` ## Configuration -Ephemeral key format in the payload and shared key in the key derivation can be configured as compressed or uncompressed format. +Following configurations are available. + +- Elliptic curve: secp256k1 or curve25519 (x25519/ed25519) +- Ephemeral key format in the payload: compressed or uncompressed (only for secp256k1) +- Shared elliptic curve key format in the key derivation: compressed or uncompressed (only for secp256k1) +- Symmetric cipher algorithm: AES-256-GCM or XChaCha20-Poly1305 +- Symmetric nonce length: 12 or 16 bytes (only for AES-256-GCM) + +For compatibility, make sure different applications share the same configuration. ```ts +export type EllipticCurve = "secp256k1" | "x25519" | "ed25519"; export type SymmetricAlgorithm = "aes-256-gcm" | "xchacha20"; -export type NonceLength = 12 | 16; // bytes. Only for aes-256-gcm +export type NonceLength = 12 | 16; class Config { + ellipticCurve: EllipticCurve = "secp256k1"; isEphemeralKeyCompressed: boolean = false; isHkdfKeyCompressed: boolean = false; symmetricAlgorithm: SymmetricAlgorithm = "aes-256-gcm"; @@ -107,15 +121,32 @@ class Config { export const ECIES_CONFIG = new Config(); ``` -For example, if you set `isEphemeralKeyCompressed = true`, the payload would be like: `33 Bytes + AES` instead of `65 Bytes + AES`. +### Elliptic curve configuration + +If you set `ellipticCurve = "x25519"` or `ellipticCurve = "ed25519"`, x25519 (key exchange function on curve25519) or ed25519 (signature algorithm on curve25519) will be used for key exchange instead of secp256k1. + +In this case, the payload would always be: `32 Bytes + AES` regardless of `isEphemeralKeyCompressed`. + +> If you don't know how to choose between x25519 and ed25519, just use x25519 for efficiency. + +### Secp256k1-specific configuration + +If you set `isEphemeralKeyCompressed = true`, the payload would be: `33 Bytes + AES` instead of `65 Bytes + AES`. If you set `isHkdfKeyCompressed = true`, the hkdf key would be derived from `ephemeral public key (compressed) + shared public key (compressed)` instead of `ephemeral public key (uncompressed) + shared public key (uncompressed)`. +### Symmetric cipher configuration + If you set `symmetricAlgorithm = "xchacha20"`, plaintext data will encrypted with XChaCha20-Poly1305. -If you set `symmetricNonceLength = 12`, then the nonce of aes-256-gcm would be 12 bytes. XChaCha20-Poly1305's nonce is always 24 bytes. +If you set `symmetricNonceLength = 12`, the nonce of AES-256-GCM would be 12 bytes. XChaCha20-Poly1305's nonce is always 24 bytes. -For compatibility, make sure different applications share the same configuration. +## Security Audit + +Following dependencies are audited: + +- [noble-curves](https://github.com/paulmillr/noble-curves/tree/main/audit) +- [noble-hashes](https://github.com/paulmillr/noble-hashes#security) ## Changelog diff --git a/package.json b/package.json index a6cd437..e79b14e 100644 --- a/package.json +++ b/package.json @@ -34,13 +34,13 @@ "test": "jest" }, "dependencies": { - "@noble/ciphers": "^0.2.0", + "@noble/ciphers": "^0.3.0", "@noble/curves": "^1.2.0", "@noble/hashes": "^1.3.2" }, "devDependencies": { "@types/jest": "^29.5.4", - "@types/node": "^20.5.6", + "@types/node": "^20.5.8", "@types/node-fetch": "^2.6.4", "https-proxy-agent": "^7.0.1", "jest": "^29.6.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10ad0f6..ec48220 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@noble/ciphers': - specifier: ^0.2.0 - version: 0.2.0 + specifier: ^0.3.0 + version: 0.3.0 '@noble/curves': specifier: ^1.2.0 version: 1.2.0 @@ -20,8 +20,8 @@ devDependencies: specifier: ^29.5.4 version: 29.5.4 '@types/node': - specifier: ^20.5.6 - version: 20.5.6 + specifier: ^20.5.8 + version: 20.5.8 '@types/node-fetch': specifier: ^2.6.4 version: 2.6.4 @@ -30,7 +30,7 @@ devDependencies: version: 7.0.1 jest: specifier: ^29.6.4 - version: 29.6.4(@types/node@20.5.6)(ts-node@10.9.1) + version: 29.6.4(@types/node@20.5.8)(ts-node@10.9.1) node-fetch: specifier: ^2.7.0 version: 2.7.0 @@ -39,7 +39,7 @@ devDependencies: version: 29.1.1(@babel/core@7.22.11)(jest@29.6.4)(typescript@5.2.2) ts-node: specifier: ^10.9.1 - version: 10.9.1(@types/node@20.5.6)(typescript@5.2.2) + version: 10.9.1(@types/node@20.5.8)(typescript@5.2.2) typescript: specifier: ^5.2.2 version: 5.2.2 @@ -54,11 +54,11 @@ packages: '@jridgewell/trace-mapping': 0.3.19 dev: true - /@babel/code-frame@7.22.10: - resolution: {integrity: sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==} + /@babel/code-frame@7.22.13: + resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/highlight': 7.22.10 + '@babel/highlight': 7.22.13 chalk: 2.4.2 dev: true @@ -72,12 +72,12 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@ampproject/remapping': 2.2.1 - '@babel/code-frame': 7.22.10 + '@babel/code-frame': 7.22.13 '@babel/generator': 7.22.10 '@babel/helper-compilation-targets': 7.22.10 '@babel/helper-module-transforms': 7.22.9(@babel/core@7.22.11) '@babel/helpers': 7.22.11 - '@babel/parser': 7.22.11 + '@babel/parser': 7.22.14 '@babel/template': 7.22.5 '@babel/traverse': 7.22.11 '@babel/types': 7.22.11 @@ -197,8 +197,8 @@ packages: - supports-color dev: true - /@babel/highlight@7.22.10: - resolution: {integrity: sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==} + /@babel/highlight@7.22.13: + resolution: {integrity: sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==} engines: {node: '>=6.9.0'} dependencies: '@babel/helper-validator-identifier': 7.22.5 @@ -206,8 +206,8 @@ packages: js-tokens: 4.0.0 dev: true - /@babel/parser@7.22.11: - resolution: {integrity: sha512-R5zb8eJIBPJriQtbH/htEQy4k7E2dHWlD2Y2VT07JCzwYZHBxV5ZYtM0UhXSNMT74LyxuM+b1jdL7pSesXbC/g==} + /@babel/parser@7.22.14: + resolution: {integrity: sha512-1KucTHgOvaw/LzCVrEOAyXkr9rQlp0A1HiHRYnSUE9dmb8PvPW7o5sscg+5169r54n3vGlbx6GevTE/Iw/P3AQ==} engines: {node: '>=6.0.0'} hasBin: true dependencies: @@ -347,8 +347,8 @@ packages: resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.22.10 - '@babel/parser': 7.22.11 + '@babel/code-frame': 7.22.13 + '@babel/parser': 7.22.14 '@babel/types': 7.22.11 dev: true @@ -356,13 +356,13 @@ packages: resolution: {integrity: sha512-mzAenteTfomcB7mfPtyi+4oe5BZ6MXxWcn4CX+h4IRJ+OOGXBrWU6jDQavkQI9Vuc5P+donFabBfFCcmWka9lQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.22.10 + '@babel/code-frame': 7.22.13 '@babel/generator': 7.22.10 '@babel/helper-environment-visitor': 7.22.5 '@babel/helper-function-name': 7.22.5 '@babel/helper-hoist-variables': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.22.11 + '@babel/parser': 7.22.14 '@babel/types': 7.22.11 debug: 4.3.4 globals: 11.12.0 @@ -411,7 +411,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.5.6 + '@types/node': 20.5.8 chalk: 4.1.2 jest-message-util: 29.6.3 jest-util: 29.6.3 @@ -432,14 +432,14 @@ packages: '@jest/test-result': 29.6.4 '@jest/transform': 29.6.4 '@jest/types': 29.6.3 - '@types/node': 20.5.6 + '@types/node': 20.5.8 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.8.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.6.3 - jest-config: 29.6.4(@types/node@20.5.6)(ts-node@10.9.1) + jest-config: 29.6.4(@types/node@20.5.8)(ts-node@10.9.1) jest-haste-map: 29.6.4 jest-message-util: 29.6.3 jest-regex-util: 29.6.3 @@ -467,7 +467,7 @@ packages: dependencies: '@jest/fake-timers': 29.6.4 '@jest/types': 29.6.3 - '@types/node': 20.5.6 + '@types/node': 20.5.8 jest-mock: 29.6.3 dev: true @@ -494,7 +494,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.5.6 + '@types/node': 20.5.8 jest-message-util: 29.6.3 jest-mock: 29.6.3 jest-util: 29.6.3 @@ -527,7 +527,7 @@ packages: '@jest/transform': 29.6.4 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.19 - '@types/node': 20.5.6 + '@types/node': 20.5.8 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -615,7 +615,7 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.5.6 + '@types/node': 20.5.8 '@types/yargs': 17.0.24 chalk: 4.1.2 dev: true @@ -657,8 +657,8 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /@noble/ciphers@0.2.0: - resolution: {integrity: sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==} + /@noble/ciphers@0.3.0: + resolution: {integrity: sha512-ldbrnOjmNRwFdXcTM6uXDcxpMIFrbzAWNnpBPp4oTJTFF0XByGD6vf45WrehZGXRQTRVV+Zm8YP+EgEf+e4cWA==} dev: false /@noble/curves@1.2.0: @@ -707,7 +707,7 @@ packages: /@types/babel__core@7.20.1: resolution: {integrity: sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==} dependencies: - '@babel/parser': 7.22.11 + '@babel/parser': 7.22.14 '@babel/types': 7.22.11 '@types/babel__generator': 7.6.4 '@types/babel__template': 7.4.1 @@ -723,7 +723,7 @@ packages: /@types/babel__template@7.4.1: resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} dependencies: - '@babel/parser': 7.22.11 + '@babel/parser': 7.22.14 '@babel/types': 7.22.11 dev: true @@ -736,7 +736,7 @@ packages: /@types/graceful-fs@4.1.6: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: - '@types/node': 20.5.6 + '@types/node': 20.5.8 dev: true /@types/istanbul-lib-coverage@2.0.4: @@ -765,12 +765,12 @@ packages: /@types/node-fetch@2.6.4: resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==} dependencies: - '@types/node': 20.5.6 + '@types/node': 20.5.8 form-data: 3.0.1 dev: true - /@types/node@20.5.6: - resolution: {integrity: sha512-Gi5wRGPbbyOTX+4Y2iULQ27oUPrefaB0PxGQJnfyWN3kvEDGM3mIB5M/gQLmitZf7A9FmLeaqxD3L1CXpm3VKQ==} + /@types/node@20.5.8: + resolution: {integrity: sha512-eajsR9aeljqNhK028VG0Wuw+OaY5LLxYmxeoXynIoE6jannr9/Ucd1LL0hSSoafk5LTYG+FfqsyGt81Q6Zkybw==} dev: true /@types/stack-utils@2.0.1: @@ -955,8 +955,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001523 - electron-to-chromium: 1.4.502 + caniuse-lite: 1.0.30001525 + electron-to-chromium: 1.4.508 node-releases: 2.0.13 update-browserslist-db: 1.0.11(browserslist@4.21.10) dev: true @@ -993,8 +993,8 @@ packages: engines: {node: '>=10'} dev: true - /caniuse-lite@1.0.30001523: - resolution: {integrity: sha512-I5q5cisATTPZ1mc588Z//pj/Ox80ERYDfR71YnvY7raS/NOk8xXlZcB0sF7JdqaV//kOaa6aus7lRfpdnt1eBA==} + /caniuse-lite@1.0.30001525: + resolution: {integrity: sha512-/3z+wB4icFt3r0USMwxujAqRvaD/B7rvGTsKhbhSQErVrJvkZCLhgNLJxU8MevahQVH6hCU9FsHdNUFbiwmE7Q==} dev: true /chalk@2.4.2: @@ -1145,8 +1145,8 @@ packages: engines: {node: '>=0.3.1'} dev: true - /electron-to-chromium@1.4.502: - resolution: {integrity: sha512-xqeGw3Gr6o3uyHy/yKjdnDQHY2RQvXcGC2cfHjccK1IGkH6cX1WQBN8EeC/YpwPhGkBaikDTecJ8+ssxSVRQlw==} + /electron-to-chromium@1.4.508: + resolution: {integrity: sha512-FFa8QKjQK/A5QuFr2167myhMesGrhlOBD+3cYNxO9/S4XzHEXesyTD/1/xF644gC8buFPz3ca6G1LOQD0tZrrg==} dev: true /emittery@0.13.1: @@ -1411,7 +1411,7 @@ packages: engines: {node: '>=8'} dependencies: '@babel/core': 7.22.11 - '@babel/parser': 7.22.11 + '@babel/parser': 7.22.14 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 semver: 6.3.1 @@ -1424,7 +1424,7 @@ packages: engines: {node: '>=10'} dependencies: '@babel/core': 7.22.11 - '@babel/parser': 7.22.11 + '@babel/parser': 7.22.14 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 semver: 7.5.4 @@ -1477,7 +1477,7 @@ packages: '@jest/expect': 29.6.4 '@jest/test-result': 29.6.4 '@jest/types': 29.6.3 - '@types/node': 20.5.6 + '@types/node': 20.5.8 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -1498,7 +1498,7 @@ packages: - supports-color dev: true - /jest-cli@29.6.4(@types/node@20.5.6)(ts-node@10.9.1): + /jest-cli@29.6.4(@types/node@20.5.8)(ts-node@10.9.1): resolution: {integrity: sha512-+uMCQ7oizMmh8ZwRfZzKIEszFY9ksjjEQnTEMTaL7fYiL3Kw4XhqT9bYh+A4DQKUb67hZn2KbtEnDuHvcgK4pQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -1515,7 +1515,7 @@ packages: exit: 0.1.2 graceful-fs: 4.2.11 import-local: 3.1.0 - jest-config: 29.6.4(@types/node@20.5.6)(ts-node@10.9.1) + jest-config: 29.6.4(@types/node@20.5.8)(ts-node@10.9.1) jest-util: 29.6.3 jest-validate: 29.6.3 prompts: 2.4.2 @@ -1527,7 +1527,7 @@ packages: - ts-node dev: true - /jest-config@29.6.4(@types/node@20.5.6)(ts-node@10.9.1): + /jest-config@29.6.4(@types/node@20.5.8)(ts-node@10.9.1): resolution: {integrity: sha512-JWohr3i9m2cVpBumQFv2akMEnFEPVOh+9L2xIBJhJ0zOaci2ZXuKJj0tgMKQCBZAKA09H049IR4HVS/43Qb19A==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -1542,7 +1542,7 @@ packages: '@babel/core': 7.22.11 '@jest/test-sequencer': 29.6.4 '@jest/types': 29.6.3 - '@types/node': 20.5.6 + '@types/node': 20.5.8 babel-jest: 29.6.4(@babel/core@7.22.11) chalk: 4.1.2 ci-info: 3.8.0 @@ -1562,7 +1562,7 @@ packages: pretty-format: 29.6.3 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1(@types/node@20.5.6)(typescript@5.2.2) + ts-node: 10.9.1(@types/node@20.5.8)(typescript@5.2.2) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -1603,7 +1603,7 @@ packages: '@jest/environment': 29.6.4 '@jest/fake-timers': 29.6.4 '@jest/types': 29.6.3 - '@types/node': 20.5.6 + '@types/node': 20.5.8 jest-mock: 29.6.3 jest-util: 29.6.3 dev: true @@ -1619,7 +1619,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.6 - '@types/node': 20.5.6 + '@types/node': 20.5.8 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -1654,7 +1654,7 @@ packages: resolution: {integrity: sha512-FtzaEEHzjDpQp51HX4UMkPZjy46ati4T5pEMyM6Ik48ztu4T9LQplZ6OsimHx7EuM9dfEh5HJa6D3trEftu3dA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/code-frame': 7.22.10 + '@babel/code-frame': 7.22.13 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.1 chalk: 4.1.2 @@ -1670,7 +1670,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.5.6 + '@types/node': 20.5.8 jest-util: 29.6.3 dev: true @@ -1725,7 +1725,7 @@ packages: '@jest/test-result': 29.6.4 '@jest/transform': 29.6.4 '@jest/types': 29.6.3 - '@types/node': 20.5.6 + '@types/node': 20.5.8 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -1756,7 +1756,7 @@ packages: '@jest/test-result': 29.6.4 '@jest/transform': 29.6.4 '@jest/types': 29.6.3 - '@types/node': 20.5.6 + '@types/node': 20.5.8 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.2 @@ -1808,7 +1808,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.5.6 + '@types/node': 20.5.8 chalk: 4.1.2 ci-info: 3.8.0 graceful-fs: 4.2.11 @@ -1833,7 +1833,7 @@ packages: dependencies: '@jest/test-result': 29.6.4 '@jest/types': 29.6.3 - '@types/node': 20.5.6 + '@types/node': 20.5.8 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -1845,13 +1845,13 @@ packages: resolution: {integrity: sha512-6dpvFV4WjcWbDVGgHTWo/aupl8/LbBx2NSKfiwqf79xC/yeJjKHT1+StcKy/2KTmW16hE68ccKVOtXf+WZGz7Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.5.6 + '@types/node': 20.5.8 jest-util: 29.6.3 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest@29.6.4(@types/node@20.5.6)(ts-node@10.9.1): + /jest@29.6.4(@types/node@20.5.8)(ts-node@10.9.1): resolution: {integrity: sha512-tEFhVQFF/bzoYV1YuGyzLPZ6vlPrdfvDmmAxudA1dLEuiztqg2Rkx20vkKY32xiDROcD2KXlgZ7Cu8RPeEHRKw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -1864,7 +1864,7 @@ packages: '@jest/core': 29.6.4(ts-node@10.9.1) '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.6.4(@types/node@20.5.6)(ts-node@10.9.1) + jest-cli: 29.6.4(@types/node@20.5.8)(ts-node@10.9.1) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -2073,7 +2073,7 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} dependencies: - '@babel/code-frame': 7.22.10 + '@babel/code-frame': 7.22.13 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -2354,7 +2354,7 @@ packages: '@babel/core': 7.22.11 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.6.4(@types/node@20.5.6)(ts-node@10.9.1) + jest: 29.6.4(@types/node@20.5.8)(ts-node@10.9.1) jest-util: 29.6.3 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -2364,7 +2364,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-node@10.9.1(@types/node@20.5.6)(typescript@5.2.2): + /ts-node@10.9.1(@types/node@20.5.8)(typescript@5.2.2): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -2383,7 +2383,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.5.6 + '@types/node': 20.5.8 acorn: 8.10.0 acorn-walk: 8.2.0 arg: 4.1.3 diff --git a/src/config.ts b/src/config.ts index 5111929..3419bab 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,22 +1,41 @@ -import { COMPRESSED_PUBLIC_KEY_SIZE, UNCOMPRESSED_PUBLIC_KEY_SIZE } from "./consts"; +import { + COMPRESSED_PUBLIC_KEY_SIZE, + CURVE25519_PUBLIC_KEY_SIZE, + UNCOMPRESSED_PUBLIC_KEY_SIZE, +} from "./consts"; +export type EllipticCurve = "secp256k1" | "x25519" | "ed25519"; export type SymmetricAlgorithm = "aes-256-gcm" | "xchacha20"; -export type NonceLength = 12 | 16; // bytes. Only for aes-256-gcm +export type NonceLength = 12 | 16; class Config { - isEphemeralKeyCompressed: boolean = false; - isHkdfKeyCompressed: boolean = false; + ellipticCurve: EllipticCurve = "secp256k1"; + isEphemeralKeyCompressed: boolean = false; // secp256k1 only + isHkdfKeyCompressed: boolean = false; // secp256k1 only symmetricAlgorithm: SymmetricAlgorithm = "aes-256-gcm"; - symmetricNonceLength: NonceLength = 16; + symmetricNonceLength: NonceLength = 16; // aes-256-gcm only } export const ECIES_CONFIG = new Config(); +export const ellipticCurve = () => ECIES_CONFIG.ellipticCurve; export const isEphemeralKeyCompressed = () => ECIES_CONFIG.isEphemeralKeyCompressed; export const isHkdfKeyCompressed = () => ECIES_CONFIG.isHkdfKeyCompressed; -export const ephemeralKeySize = () => - ECIES_CONFIG.isEphemeralKeyCompressed - ? COMPRESSED_PUBLIC_KEY_SIZE - : UNCOMPRESSED_PUBLIC_KEY_SIZE; export const symmetricAlgorithm = () => ECIES_CONFIG.symmetricAlgorithm; export const symmetricNonceLength = () => ECIES_CONFIG.symmetricNonceLength; + +export const ephemeralKeySize = () => { + const mapping = { + secp256k1: ECIES_CONFIG.isEphemeralKeyCompressed + ? COMPRESSED_PUBLIC_KEY_SIZE + : UNCOMPRESSED_PUBLIC_KEY_SIZE, + x25519: CURVE25519_PUBLIC_KEY_SIZE, + ed25519: CURVE25519_PUBLIC_KEY_SIZE, + }; + + if (ECIES_CONFIG.ellipticCurve in mapping) { + return mapping[ECIES_CONFIG.ellipticCurve]; + } else { + throw new Error("Not implemented"); + } +}; diff --git a/src/consts.ts b/src/consts.ts index 6ac342c..c383c83 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -1,7 +1,10 @@ +// elliptic +export const SECRET_KEY_LENGTH = 32; export const COMPRESSED_PUBLIC_KEY_SIZE = 33; export const UNCOMPRESSED_PUBLIC_KEY_SIZE = 65; export const ETH_PUBLIC_KEY_SIZE = 64; -export const SECRET_KEY_LENGTH = 32; -export const ONE = BigInt(1); +export const CURVE25519_PUBLIC_KEY_SIZE = 32; + +// symmetric export const XCHACHA20_NONCE_LENGTH = 24; export const AEAD_TAG_LENGTH = 16; diff --git a/src/index.ts b/src/index.ts index fb0b350..884eace 100644 --- a/src/index.ts +++ b/src/index.ts @@ -41,6 +41,7 @@ export { ECIES_CONFIG } from "./config"; export { PrivateKey, PublicKey } from "./keys"; export const utils = { + // TODO: review these before 0.5.0 aesDecrypt, aesEncrypt, decodeHex, diff --git a/src/keys/PrivateKey.ts b/src/keys/PrivateKey.ts index 5684073..d8f7c0f 100644 --- a/src/keys/PrivateKey.ts +++ b/src/keys/PrivateKey.ts @@ -9,27 +9,32 @@ import { getValidSecret, isValidPrivateKey, } from "../utils"; -import PublicKey from "./PublicKey"; +import { PublicKey } from "./PublicKey"; -export default class PrivateKey { +export class PrivateKey { public static fromHex(hex: string): PrivateKey { return new PrivateKey(decodeHex(hex)); } - public readonly secret: Buffer; // TODO: Uint8Array + private readonly data: Uint8Array; public readonly publicKey: PublicKey; + get secret(): Buffer { + // TODO: Uint8Array + return Buffer.from(this.data); + } + constructor(secret?: Uint8Array) { const sk = secret === undefined ? getValidSecret() : secret; if (!isValidPrivateKey(sk)) { throw new Error("Invalid private key"); } - this.secret = Buffer.from(sk); + this.data = sk; this.publicKey = new PublicKey(getPublicKey(sk)); } public toHex(): string { - return bytesToHex(this.secret); + return bytesToHex(this.data); } public encapsulate(pk: PublicKey): Uint8Array { @@ -46,10 +51,10 @@ export default class PrivateKey { } public multiply(pk: PublicKey, compressed: boolean = false): Uint8Array { - return getSharedPoint(this.secret, pk.compressed, compressed); + return getSharedPoint(this.data, pk.compressed, compressed); } public equals(other: PrivateKey): boolean { - return equalBytes(this.secret, other.secret); + return equalBytes(this.data, other.data); } } diff --git a/src/keys/PublicKey.ts b/src/keys/PublicKey.ts index 2cf46fd..99c89a3 100644 --- a/src/keys/PublicKey.ts +++ b/src/keys/PublicKey.ts @@ -1,34 +1,33 @@ import { bytesToHex, equalBytes } from "@noble/ciphers/utils"; import { isHkdfKeyCompressed } from "../config"; -import { ETH_PUBLIC_KEY_SIZE, ONE } from "../consts"; -import { decodeHex, getSharedKey, getSharedPoint } from "../utils"; -import PrivateKey from "./PrivateKey"; +import { convertPublicKeyFormat, getSharedKey, hexToPublicKey } from "../utils"; +import { PrivateKey } from "./PrivateKey"; -export default class PublicKey { +export class PublicKey { public static fromHex(hex: string): PublicKey { - const decoded = decodeHex(hex); - if (decoded.length === ETH_PUBLIC_KEY_SIZE) { - // eth public key - const fixed = new Uint8Array(1 + decoded.length); - fixed.set([0x04]); - fixed.set(decoded, 1); - return new PublicKey(fixed); - } - return new PublicKey(decoded); + return new PublicKey(hexToPublicKey(hex)); } - public readonly uncompressed: Buffer; // TODO: Uint8Array - public readonly compressed: Buffer; + private readonly data: Uint8Array; // always compressed if secp256k1 + + get uncompressed(): Buffer { + // TODO: Uint8Array + return Buffer.from(convertPublicKeyFormat(this.data, false)); + } + + get compressed(): Buffer { + // TODO: Uint8Array + return Buffer.from(this.data); + } - constructor(buffer: Uint8Array) { - this.uncompressed = Buffer.from(getSharedPoint(ONE, buffer, false)); - this.compressed = Buffer.from(getSharedPoint(ONE, buffer, true)); + constructor(data: Uint8Array) { + this.data = convertPublicKeyFormat(data, true); } public toHex(compressed: boolean = true): string { if (compressed) { - return bytesToHex(this.compressed); + return bytesToHex(this.data); } else { return bytesToHex(this.uncompressed); } @@ -38,7 +37,7 @@ export default class PublicKey { let senderPoint: Uint8Array; let sharedPoint: Uint8Array; if (isHkdfKeyCompressed()) { - senderPoint = this.compressed; + senderPoint = this.data; sharedPoint = sk.multiply(this, true); } else { senderPoint = this.uncompressed; @@ -48,6 +47,6 @@ export default class PublicKey { } public equals(other: PublicKey): boolean { - return equalBytes(this.uncompressed, other.uncompressed); + return equalBytes(this.data, other.data); } } diff --git a/src/keys/index.ts b/src/keys/index.ts index b6b8014..fe7b616 100644 --- a/src/keys/index.ts +++ b/src/keys/index.ts @@ -1,4 +1,4 @@ // treat Buffer as Uint8array, i.e. no call of Buffer specific functions // finally Uint8Array only -export { default as PrivateKey } from "./PrivateKey"; -export { default as PublicKey } from "./PublicKey"; +export { PrivateKey } from "./PrivateKey"; +export { PublicKey } from "./PublicKey"; diff --git a/src/utils/compat.ts b/src/utils/compat.ts index e3a309e..2b60c85 100644 --- a/src/utils/compat.ts +++ b/src/utils/compat.ts @@ -4,7 +4,6 @@ import { createCipheriv, createDecipheriv } from "crypto"; import { AEAD_TAG_LENGTH } from "../consts"; // make node's aes compatible with `@noble/ciphers` -// the implementation of `@noble/ciphers` does not support aad yet export function aes256gcm( key: Uint8Array, nonce: Uint8Array, @@ -12,6 +11,9 @@ export function aes256gcm( ): Cipher { const encrypt = (plainText: Uint8Array) => { const cipher = createCipheriv("aes-256-gcm", key, nonce); + if (AAD) { + cipher.setAAD(AAD); + } const updated = cipher.update(plainText); const finalized = cipher.final(); const tag = cipher.getAuthTag(); @@ -23,6 +25,9 @@ export function aes256gcm( const tag = cipherText.subarray(-AEAD_TAG_LENGTH); const decipher = createDecipheriv("aes-256-gcm", key, nonce); + if (AAD) { + decipher.setAAD(AAD); + } decipher.setAuthTag(tag); const updated = decipher.update(encrypted); const finalized = decipher.final(); diff --git a/src/utils/elliptic.ts b/src/utils/elliptic.ts index fac1a87..6154706 100644 --- a/src/utils/elliptic.ts +++ b/src/utils/elliptic.ts @@ -1,14 +1,13 @@ import { concatBytes } from "@noble/ciphers/utils"; import { randomBytes } from "@noble/ciphers/webcrypto/utils"; +import { ed25519, x25519 } from "@noble/curves/ed25519"; import { secp256k1 } from "@noble/curves/secp256k1"; -import { SECRET_KEY_LENGTH } from "../consts"; +import { ellipticCurve } from "../config"; +import { ETH_PUBLIC_KEY_SIZE, SECRET_KEY_LENGTH } from "../consts"; +import { decodeHex } from "./hex"; import { deriveKey } from "./symmetric"; -export function isValidPrivateKey(secret: Uint8Array) { - return secp256k1.utils.isValidPrivateKey(secret); -} - export function getValidSecret(): Uint8Array { let key: Uint8Array; do { @@ -17,16 +16,22 @@ export function getValidSecret(): Uint8Array { return key; } -export function getPublicKey(secret: Uint8Array): Uint8Array { - return secp256k1.getPublicKey(secret); +export function isValidPrivateKey(secret: Uint8Array): boolean { + // only key in (0, group order) is valid on secp256k1 + // any 32-byte key is valid on curve25519 + return toCurve( + (curve) => curve.utils.isValidPrivateKey(secret), + () => true, + () => true + ); } -export function getSharedPoint( - skRaw: Uint8Array | bigint, - pkRaw: Uint8Array, - compressed: boolean -): Uint8Array { - return secp256k1.getSharedSecret(skRaw, pkRaw, compressed); +export function getPublicKey(secret: Uint8Array): Uint8Array { + return toCurve( + (curve) => curve.getPublicKey(secret), + (curve) => curve.getPublicKey(secret), + (curve) => curve.getPublicKey(secret) + ); } export function getSharedKey( @@ -35,3 +40,64 @@ export function getSharedKey( ): Uint8Array { return deriveKey(concatBytes(ephemeralSenderPoint, sharedPoint)); } + +export function getSharedPoint( + skRaw: Uint8Array, + pkRaw: Uint8Array, + compressed?: boolean +): Uint8Array { + return toCurve( + (curve) => curve.getSharedSecret(skRaw, pkRaw, compressed), + (curve) => curve.getSharedSecret(skRaw, pkRaw), + (curve) => { + // Note: scalar is hashed from skRaw + const { scalar } = curve.utils.getExtendedPublicKey(skRaw); + const point = curve.ExtendedPoint.fromHex(pkRaw).multiply(scalar); + return point.toRawBytes(); + } + ); +} + +export function convertPublicKeyFormat( + pkRaw: Uint8Array, + compressed: boolean +): Uint8Array { + return toCurve( + (curve) => curve.getSharedSecret(BigInt(1), pkRaw, compressed), // only for secp256k1 + () => pkRaw, + () => pkRaw + ); +} + +export function hexToPublicKey(hex: string): Uint8Array { + const decoded = decodeHex(hex); + return toCurve( + () => { + if (decoded.length === ETH_PUBLIC_KEY_SIZE) { + const fixed = new Uint8Array(1 + decoded.length); + fixed.set([0x04]); + fixed.set(decoded, 1); + return fixed; + } + return decoded; + }, + () => decoded, + () => decoded + ); +} + +function toCurve( + secp256k1Callback: (curve: typeof secp256k1) => T, + x25519Callback: (curve: typeof x25519) => T, + ed25519Callback: (curve: typeof ed25519) => T +) { + if (ellipticCurve() === "secp256k1") { + return secp256k1Callback(secp256k1); + } else if (ellipticCurve() === "x25519") { + return x25519Callback(x25519); + } else if (ellipticCurve() === "ed25519") { + return ed25519Callback(ed25519); + } else { + throw new Error("Not implemented"); + } +} diff --git a/tests/crypt.test.ts b/tests/crypt.test.ts index b63b143..70d6613 100644 --- a/tests/crypt.test.ts +++ b/tests/crypt.test.ts @@ -26,8 +26,8 @@ function checkHex(sk: PrivateKey) { expect(decrypt(sk.secret, encrypted).toString()).toBe(TEXT); } -describe("test encrypt and decrypt", () => { - it("tests encrypt/decrypt", () => { +describe("test random encrypt and decrypt", () => { + function testRandom() { const sk1 = new PrivateKey(); check(sk1); checkHex(sk1); @@ -35,50 +35,68 @@ describe("test encrypt and decrypt", () => { const sk2 = new PrivateKey(); check(sk2, true); checkHex(sk2); - }); + } - it("tests known sk pk", () => { - const sk = PrivateKey.fromHex( - "5b5b1a0ff51e4350badd6f58d9e6fa6f57fbdbde6079d12901770dda3b803081" - ); - const pk = PublicKey.fromHex( - "048e41409f2e109f2d704f0afd15d1ab53935fd443729913a7e8536b4cef8cf5773d4db7bbd99e9ed64595e24a251c9836f35d4c9842132443c17f6d501b3410d2" - ); - const enc = encrypt(pk.toHex(), Buffer.from(TEXT)); - expect(decrypt(sk.toHex(), enc).toString()).toBe(TEXT); + it("tests default", () => { + testRandom(); }); - it("tests config can be changed", () => { + it("tests compressed ephemeral and hkdf key with 12 bytes nonce", () => { ECIES_CONFIG.isEphemeralKeyCompressed = true; ECIES_CONFIG.isHkdfKeyCompressed = true; ECIES_CONFIG.symmetricNonceLength = 12; - const sk1 = new PrivateKey(); - check(sk1); - - const sk2 = new PrivateKey(); - checkHex(sk2); + testRandom(); ECIES_CONFIG.isEphemeralKeyCompressed = false; ECIES_CONFIG.isHkdfKeyCompressed = false; ECIES_CONFIG.symmetricNonceLength = 16; }); - it("tests encrypt/decrypt chacha", () => { + it("tests compressed ephemeral key and chacha", () => { ECIES_CONFIG.symmetricAlgorithm = "xchacha20"; ECIES_CONFIG.isEphemeralKeyCompressed = true; - const sk1 = new PrivateKey(); - check(sk1); - - const sk2 = new PrivateKey(); - checkHex(sk2); + testRandom(); ECIES_CONFIG.symmetricAlgorithm = "aes-256-gcm"; ECIES_CONFIG.isEphemeralKeyCompressed = false; }); - it("tests known sk pk chacha", () => { + it("tests curve25519 and chacha", () => { + ECIES_CONFIG.ellipticCurve = "x25519"; + ECIES_CONFIG.symmetricAlgorithm = "xchacha20"; + + testRandom(); + + ECIES_CONFIG.ellipticCurve = "ed25519"; + testRandom(); + + ECIES_CONFIG.symmetricAlgorithm = "aes-256-gcm"; + ECIES_CONFIG.ellipticCurve = "secp256k1"; + }); +}); + +describe("test known encrypt and decrypt", () => { + function testKnown(sk: PrivateKey, pk: PublicKey, msg: string, enc?: Uint8Array) { + if (enc === undefined) { + enc = encrypt(pk.toHex(), Buffer.from(msg)); + } + + expect(decrypt(sk.toHex(), enc).toString()).toBe(msg); + } + + it("tests default", () => { + const sk = PrivateKey.fromHex( + "5b5b1a0ff51e4350badd6f58d9e6fa6f57fbdbde6079d12901770dda3b803081" + ); + const pk = PublicKey.fromHex( + "048e41409f2e109f2d704f0afd15d1ab53935fd443729913a7e8536b4cef8cf5773d4db7bbd99e9ed64595e24a251c9836f35d4c9842132443c17f6d501b3410d2" + ); + testKnown(sk, pk, TEXT); + }); + + it("tests chacha", () => { ECIES_CONFIG.symmetricAlgorithm = "xchacha20"; const sk = PrivateKey.fromHex( @@ -87,8 +105,7 @@ describe("test encrypt and decrypt", () => { const pk = PublicKey.fromHex( "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" ); - const enc = encrypt(pk.toHex(), Buffer.from(TEXT)); - expect(decrypt(sk.toHex(), enc).toString()).toBe(TEXT); + testKnown(sk, pk, TEXT); const known = Buffer.from( decodeHex( @@ -98,8 +115,36 @@ describe("test encrypt and decrypt", () => { "bb955c8e5d4aee8572139357a091909357a8931b" ) ); - expect(decrypt(sk.toHex(), known).toString()).toBe(TEXT); + testKnown(sk, pk, TEXT, known); ECIES_CONFIG.symmetricAlgorithm = "aes-256-gcm"; }); + + it("tests x25519", () => { + ECIES_CONFIG.ellipticCurve = "x25519"; + + const sk = PrivateKey.fromHex( + "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a" + ); + const pk = PublicKey.fromHex( + "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a" + ); + testKnown(sk, pk, TEXT); + + ECIES_CONFIG.ellipticCurve = "secp256k1"; + }); + + it("tests ed25519", () => { + ECIES_CONFIG.ellipticCurve = "ed25519"; + + const sk = PrivateKey.fromHex( + "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60" + ); + const pk = PublicKey.fromHex( + "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a" + ); + testKnown(sk, pk, TEXT); + + ECIES_CONFIG.ellipticCurve = "secp256k1"; + }); }); diff --git a/tests/keys.test.ts b/tests/keys.test.ts index fbfb394..dbe0f9a 100644 --- a/tests/keys.test.ts +++ b/tests/keys.test.ts @@ -7,7 +7,7 @@ two[31] = 2; const three = new Uint8Array(32); three[31] = 3; -describe("test keys", () => { +describe("test secp256k1 keys", () => { function checkHkdf(k1: PrivateKey, k2: PrivateKey, knownHex: string) { const derived1 = k1.encapsulate(k2.publicKey); const derived2 = k1.publicKey.decapsulate(k2); diff --git a/tests/utils/compat.test.ts b/tests/utils/compat.test.ts index 44043f7..6a45cf2 100644 --- a/tests/utils/compat.test.ts +++ b/tests/utils/compat.test.ts @@ -11,22 +11,22 @@ const encoder = new TextEncoder(); describe("test compat utils", () => { const msg = encoder.encode(TEXT); - async function testRandom() { + async function testRandom(aad?: Uint8Array) { const key = randomBytes(); const nonce = randomBytes(16); - // @ts-ignore - const noble = aes(key, nonce); - const compat = aes256gcm(key, nonce); + const noble = aes(key, nonce, aad); + const compat = aes256gcm(key, nonce, aad); // same encryption expect(await noble.encrypt(msg)).toStrictEqual(compat.encrypt(msg)); - // noble encrypts compat decrypts + // noble encrypts, compat decrypts expect(compat.decrypt(await noble.encrypt(msg))).toStrictEqual(msg); - // noble decrypts compat encrypts + // noble decrypts, compat encrypts expect(await noble.decrypt(compat.encrypt(msg))).toStrictEqual(msg); } it("test aes random", async () => { await testRandom(); + await testRandom(randomBytes(16)); }); it("test aes known key", async () => { @@ -38,9 +38,10 @@ describe("test compat utils", () => { const encrypted = decodeHex("02d2ffed93b856f148b9"); const known = concatBytes(encrypted, tag); const msg = encoder.encode("helloworld"); + const aad = Uint8Array.from([]); - const noble = aes(key, nonce); - const compat = aes256gcm(key, nonce, Uint8Array.from([])); + const noble = aes(key, nonce, aad); + const compat = aes256gcm(key, nonce, aad); expect(compat.decrypt(known)).toStrictEqual(msg); expect(await noble.decrypt(known)).toStrictEqual(compat.decrypt(known)); }); diff --git a/tests/utils/elliptic.test.ts b/tests/utils/elliptic.test.ts index 595a9a5..5e54521 100644 --- a/tests/utils/elliptic.test.ts +++ b/tests/utils/elliptic.test.ts @@ -1,11 +1,54 @@ -import { utils } from "../../src/index"; -import { isValidPrivateKey } from "../../src/utils"; +import { ECIES_CONFIG, utils } from "../../src/index"; +import { decodeHex, getSharedPoint, isValidPrivateKey } from "../../src/utils/"; const { getValidSecret } = utils; -describe("test elliptic utils", () => { +describe("test random elliptic", () => { it("should generate valid secret", () => { const key = getValidSecret(); expect(isValidPrivateKey(key)).toBe(true); }); }); + +describe("test known elliptic", () => { + function testKnown(sk: string, pk: string, shared: string) { + const sharedPoint = getSharedPoint(decodeHex(sk), decodeHex(pk)); + expect(sharedPoint).toStrictEqual(decodeHex(shared)); + } + + it("tests x25519", () => { + ECIES_CONFIG.ellipticCurve = "x25519"; + + // https://datatracker.ietf.org/doc/html/rfc7748.html#section-6.1 + testKnown( + "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a", + "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f", + "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742" + ); + + ECIES_CONFIG.ellipticCurve = "secp256k1"; + }); + + it("tests ed25519", () => { + ECIES_CONFIG.ellipticCurve = "ed25519"; + + // scalar of sk: 3140620980319341722849076354004524857726602937622481303882784251885505225391 + // shared point: + // (10766034509508892393929108371050440292889843231095811528019173932139015419574, + // 57672573619093321322151945555557301978191423137769245365888971894976817047673) + + testKnown( + "0000000000000000000000000000000000000000000000000000000000000000", // sk + "4cb5abf6ad79fbf5abbccafcc269d85cd2651ed4b885b5869f241aedf0a5ba29", // peer pk + "79a82a4ed2cbf9cab6afbf353df0a225b58642c0c7b3760a99856bf01785817f" + ); + + testKnown( + "0000000000000000000000000000000000000000000000000000000000000001", // peer sk + "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29", + "79a82a4ed2cbf9cab6afbf353df0a225b58642c0c7b3760a99856bf01785817f" + ); + + ECIES_CONFIG.ellipticCurve = "secp256k1"; + }); +}); diff --git a/tests/utils/symmetric.test.ts b/tests/utils/symmetric.test.ts index 54db2ef..ad52622 100644 --- a/tests/utils/symmetric.test.ts +++ b/tests/utils/symmetric.test.ts @@ -10,23 +10,14 @@ const TEXT = "helloworld🌍"; const encoder = new TextEncoder(); const decoder = new TextDecoder(); -describe("test symmetric utils", () => { +describe("test random symmetric", () => { function testRandomKey() { const key = randomBytes(32); const data = encoder.encode(TEXT); expect(data).toEqual(aesDecrypt(key, aesEncrypt(key, data))); } - it("tests hkdf with know key", () => { - const knownKey = Uint8Array.from( - decodeHex("0x8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d") - ); - expect(knownKey).toEqual( - deriveKey(decodeHex("0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")) - ); - }); - - it("tests aes with random key", () => { + it("tests aes", () => { testRandomKey(); }); @@ -38,24 +29,7 @@ describe("test symmetric utils", () => { ECIES_CONFIG.symmetricNonceLength = 16; }); - it("tests xchacha20 decrypt with known key", () => { - ECIES_CONFIG.symmetricAlgorithm = "xchacha20"; - - const key = decodeHex( - "27bd6ec46292a3b421cdaf8a3f0ca759cbc67bcbe7c5855aa0d1e0700fd0e828" - ); - const nonce = decodeHex("0xfbd5dd10431af533c403d6f4fa629931e5f31872d2f7e7b6"); - const tag = decodeHex("0X5b5ccc27324af03b7ca92dd067ad6eb5"); - const encrypted = decodeHex("aa0664f3c00a09d098bf"); - const data = concatBytes(nonce, tag, encrypted); - - const decrypted = aesDecrypt(key, data); - expect(decoder.decode(decrypted)).toBe("helloworld"); - - ECIES_CONFIG.symmetricAlgorithm = "aes-256-gcm"; - }); - - it("tests xchacha20 with random key", () => { + it("tests xchacha20", () => { ECIES_CONFIG.symmetricAlgorithm = "xchacha20"; testRandomKey(); @@ -75,3 +49,31 @@ describe("test symmetric utils", () => { ECIES_CONFIG.symmetricAlgorithm = "aes-256-gcm"; }); }); + +describe("test known symmetric", () => { + it("tests hkdf with know key", () => { + const knownKey = Uint8Array.from( + decodeHex("0x8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d") + ); + expect(knownKey).toEqual( + deriveKey(decodeHex("0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")) + ); + }); + + it("tests xchacha20 decrypt with known key", () => { + ECIES_CONFIG.symmetricAlgorithm = "xchacha20"; + + const key = decodeHex( + "27bd6ec46292a3b421cdaf8a3f0ca759cbc67bcbe7c5855aa0d1e0700fd0e828" + ); + const nonce = decodeHex("0xfbd5dd10431af533c403d6f4fa629931e5f31872d2f7e7b6"); + const tag = decodeHex("0X5b5ccc27324af03b7ca92dd067ad6eb5"); + const encrypted = decodeHex("aa0664f3c00a09d098bf"); + const data = concatBytes(nonce, tag, encrypted); + + const decrypted = aesDecrypt(key, data); + expect(decoder.decode(decrypted)).toBe("helloworld"); + + ECIES_CONFIG.symmetricAlgorithm = "aes-256-gcm"; + }); +});