diff --git a/.cspell.jsonc b/.cspell.jsonc new file mode 100644 index 0000000..dba9f15 --- /dev/null +++ b/.cspell.jsonc @@ -0,0 +1,16 @@ +{ + "words": [ + "chacha", + "ecies", + "hchacha", + "xchacha" + ], + "ignorePaths": [ + ".git", + ".github", + ".gitignore", + ".cspell.jsonc", + "LICENSE", + "package.json" + ] +} diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index ff3e3f7..4c07cbf 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -22,7 +22,7 @@ jobs: - run: pnpm install && pnpm test -- --bail 1 - - run: pnpm run build && npm publish --access public + - run: pnpm build && npm publish --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_CONFIG_PROVENANCE: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e104a62..61d188b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,9 +16,7 @@ jobs: node: [18, 20, 22] steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} @@ -26,8 +24,34 @@ jobs: cache-dependency-path: pnpm-lock.yaml - run: pnpm install && pnpm test -- --bail 1 + - run: pnpm build && npm publish --dry-run - uses: codecov/codecov-action@v4 + if: matrix.os == 'ubuntu-latest' && matrix.node == 22 with: token: ${{ secrets.CODECOV_TOKEN }} - - run: pnpm run build && npm publish --dry-run + + check-runtimes: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - run: pnpm install && pnpm build + - run: cd example && pnpm install + - run: bun run example/main.js + - run: deno run --allow-read example/main.js + - run: node example/main.js diff --git a/.gitignore b/.gitignore index aa6ef7c..b2d8c8c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,9 @@ node_modules/ coverage/ dist/ -bun.lockb -deno.lock +# example +example/bun.lockb +example/deno.lock +example/pnpm-lock.yaml .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ab2a317 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Release Notes + +## 0.2.0 + +- Add xchacha20-poly1305 support + +## 0.1.0 + +- First beta version release with aes-256-gcm and aes-256-cbc support diff --git a/README.md b/README.md index fafd39d..a379eb7 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,52 @@ # @ecies/ciphers -Node/Pure js symmetric ciphers adapter. +[![License](https://img.shields.io/github/license/ecies/js-ciphers.svg)](https://github.com/ecies/js-ciphers) +[![Npm Package](https://img.shields.io/npm/v/@ecies/ciphers.svg)](https://www.npmjs.com/package/@ecies/ciphers) +[![CI](https://img.shields.io/github/actions/workflow/status/ecies/js-ciphers/ci.yml)](https://github.com/ecies/js-ciphers/actions) +[![Codecov](https://img.shields.io/codecov/c/github/ecies/js-ciphers.svg)](https://codecov.io/gh/ecies/js-ciphers) -On browsers (or deno), it'll use `@noble/ciphers`'s implementation. +Node/Pure JavaScript symmetric ciphers adapter. -On node (or bun), it'll use `node:crypto`'s implementation. +On browsers (or deno), it'll use [`@noble/ciphers`](https://github.com/paulmillr/noble-ciphers)'s implementation for compatibility. -Check the [example](./example/) folder for the bun/deno usage. +On node (or bun), it'll use [`node:crypto`](https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options)'s implementation for efficiency. + +> [!NOTE] +> There are some limitations, see [Known limitations](#known-limitations) below. + +Check the [example](./example/) folder for bun/deno usage. + +## Quick start + +```js +import { aes256gcm } from "@ecies/ciphers/aes"; +import { randomBytes } from "@noble/ciphers/webcrypto"; + +const TEXT = "hello world🌍!"; +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +const msg = encoder.encode(TEXT); + +const key = randomBytes(); +const nonce = randomBytes(16); +const cipher = aes256gcm(key, nonce); +console.log("decrypted:", decoder.decode(cipher.decrypt(cipher.encrypt(msg)))); +``` + +The API follows `@noble/ciphers`'s API for ease of use, you can check their [examples](https://github.com/paulmillr/noble-ciphers#examples) as well. + +## Supported ciphers + +- `aes-256-gcm` + - Both 16 bytes and 12 bytes nonce are supported. +- `aes-256-cbc` + - **Only for legacy applications**. You should use `xchacha20-poly1305` or `aes-256-gcm` as possible. + - Nonce is always 16 bytes. +- `xchacha20-poly1305` + - Nonce is always 24 bytes. + +## Known limitations + +- `xchacha20-poly1305` is implemented with pure JS [`hchacha`](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#section-2.2) function and `node:crypto`'s `chacha20-poly1305`. +- Currently (Oct 2024), `node:crypto`'s `chacha20-poly1305` is not supported on deno and [bun](https://github.com/oven-sh/bun/issues/8072), `@noble/ciphers`'s implementation is used on both platforms instead. +- `deno` does not support **indirect** conditional exports. If you use this library to build another library, client code of your library probably falls back to the `node:crypto` implementation and may not work properly, specifically `aes-256-gcm` (16 bytes nonce) and `chacha20-poly1305`. diff --git a/example/README.md b/example/README.md index d5c22bd..d5e519f 100644 --- a/example/README.md +++ b/example/README.md @@ -1,9 +1,17 @@ -# example +# runtime-example + +## Install + +Run with `bun install` (or `pnpm install`) ## bun -Run with `bun install && bun run index.ts` +Run with `bun run main.js` ## deno -Run with `deno run main.ts` +Run with `deno run --allow-read main.js` + +## node + +Run with `node main.js` diff --git a/example/bun/index.ts b/example/bun/index.ts deleted file mode 100644 index 500492d..0000000 --- a/example/bun/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { aes256gcm } from "@ecies/ciphers"; -import { randomBytes } from "@noble/ciphers/webcrypto"; - -const TEXT = "helloworld🌍"; -const encoder = new TextEncoder(); -const decoder = new TextDecoder(); -const msg = encoder.encode(TEXT); - -const key = randomBytes(); -const nonce = randomBytes(16); -const cipher = aes256gcm(key, nonce); -console.log("decrypted:", decoder.decode(cipher.decrypt(cipher.encrypt(msg)))); diff --git a/example/bun/package.json b/example/bun/package.json deleted file mode 100644 index 5446a70..0000000 --- a/example/bun/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "bun-example", - "module": "index.ts", - "type": "module", - "dependencies": { - "@ecies/ciphers": "^0.1.0" - }, - "devDependencies": { - "bun-types": "latest" - }, - "peerDependencies": { - "typescript": "^5.6.3" - } -} diff --git a/example/bun/tsconfig.json b/example/bun/tsconfig.json deleted file mode 100644 index 7556e1d..0000000 --- a/example/bun/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "lib": ["ESNext"], - "module": "esnext", - "target": "esnext", - "moduleResolution": "bundler", - "moduleDetection": "force", - "allowImportingTsExtensions": true, - "noEmit": true, - "composite": true, - "strict": true, - "downlevelIteration": true, - "skipLibCheck": true, - "jsx": "react-jsx", - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "allowJs": true, - "types": [ - "bun-types" // add Bun global - ] - } -} diff --git a/example/deno/deno.json b/example/deno/deno.json deleted file mode 100644 index 78cddac..0000000 --- a/example/deno/deno.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "tasks": { - "dev": "deno run --watch main.ts" - }, - "imports": { - "@ecies/ciphers": "npm:@ecies/ciphers@0.1.0", - "@noble/ciphers": "npm:@noble/ciphers@1.0.0" - } -} diff --git a/example/deno/main.ts b/example/deno/main.ts deleted file mode 100644 index 500492d..0000000 --- a/example/deno/main.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { aes256gcm } from "@ecies/ciphers"; -import { randomBytes } from "@noble/ciphers/webcrypto"; - -const TEXT = "helloworld🌍"; -const encoder = new TextEncoder(); -const decoder = new TextDecoder(); -const msg = encoder.encode(TEXT); - -const key = randomBytes(); -const nonce = randomBytes(16); -const cipher = aes256gcm(key, nonce); -console.log("decrypted:", decoder.decode(cipher.decrypt(cipher.encrypt(msg)))); diff --git a/example/main.js b/example/main.js new file mode 100644 index 0000000..c933587 --- /dev/null +++ b/example/main.js @@ -0,0 +1,46 @@ +import { aes256cbc, aes256gcm } from "@ecies/ciphers/aes"; +import { xchacha20 } from "@ecies/ciphers/chacha"; + +import { randomBytes } from "@noble/ciphers/webcrypto"; + +const TEXT = "hello world🌍!"; +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +const msg = encoder.encode(TEXT); + +const ciphers = [ + { + keyLength: 32, + nonceLength: 16, + callback: aes256gcm, + aad: randomBytes(16), + }, + { + keyLength: 32, + nonceLength: 12, + callback: aes256gcm, + aad: randomBytes(16), + }, + { + keyLength: 32, + nonceLength: 16, + callback: aes256cbc, + aad: undefined, + }, + { + keyLength: 32, + nonceLength: 24, + callback: xchacha20, + aad: randomBytes(16), + }, +]; + +for (const { keyLength, nonceLength, callback, aad } of ciphers) { + const key = randomBytes(keyLength); + const nonce = randomBytes(nonceLength); + const cipher = callback(key, nonce, aad); + console.log( + `${callback.name} (nonce length ${nonce.length}) decrypted:`, + decoder.decode(cipher.decrypt(cipher.encrypt(msg))) + ); +} diff --git a/example/package.json b/example/package.json new file mode 100644 index 0000000..59c8aa5 --- /dev/null +++ b/example/package.json @@ -0,0 +1,8 @@ +{ + "name": "runtime-example", + "main": "main.js", + "type": "module", + "dependencies": { + "@ecies/ciphers": "file:.." + } +} diff --git a/package.json b/package.json index ba6df4d..bfbf125 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ecies/ciphers", - "description": "Node/Pure js symmetric ciphers adapter", + "description": "Node/Pure JavaScript symmetric ciphers adapter", "license": "MIT", "author": { "name": "Weiliang Li", @@ -11,38 +11,52 @@ "type": "git", "url": "git+https://github.com/ecies/js-ciphers.git" }, - "version": "0.1.0", + "version": "0.2.0", "engines": { - "node": ">=16.0.0" + "node": ">=16", + "bun": ">=1", + "deno": ">=2" }, "keywords": [ "cryptography", - "aes" + "cipher", + "aes", + "chacha", + "xchacha20", + "xchacha20poly1305" ], - "main": "dist/node.js", - "types": "dist/node.d.ts", "files": [ "dist" ], "exports": { - "types": "./dist/node.d.ts", - "browser": "./dist/noble.js", - "deno": "./dist/noble.js", - "bun": "./dist/node.js", - "default": "./dist/node.js" + ".": null, + "./aes": { + "types": "./dist/aes/node.d.ts", + "browser": "./dist/aes/noble.js", + "deno": "./dist/aes/noble.js", + "bun": "./dist/aes/node.js", + "default": "./dist/aes/node.js" + }, + "./chacha": { + "types": "./dist/chacha/node.d.ts", + "browser": "./dist/chacha/noble.js", + "deno": "./dist/chacha/noble.js", + "bun": "./dist/chacha/noble.js", + "default": "./dist/chacha/node.js" + } }, "scripts": { "build": "npx tsc", "test": "vitest" }, - "dependencies": { + "peerDependencies": { "@noble/ciphers": "^1.0.0" }, "devDependencies": { - "@types/node": "^22.7.5", - "@vitest/coverage-v8": "^2.1.2", + "@types/node": "^22.7.6", + "@vitest/coverage-v8": "^2.1.3", "typescript": "^5.6.3", - "vitest": "^2.1.2" + "vitest": "^2.1.3" }, - "packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4" + "packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32a958d..1027c32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,17 +13,17 @@ importers: version: 1.0.0 devDependencies: '@types/node': - specifier: ^22.7.5 - version: 22.7.5 + specifier: ^22.7.6 + version: 22.7.6 '@vitest/coverage-v8': - specifier: ^2.1.2 - version: 2.1.2(vitest@2.1.2(@types/node@22.7.5)) + specifier: ^2.1.3 + version: 2.1.3(vitest@2.1.3(@types/node@22.7.6)) typescript: specifier: ^5.6.3 version: 5.6.3 vitest: - specifier: ^2.1.2 - version: 2.1.2(@types/node@22.7.5) + specifier: ^2.1.3 + version: 2.1.3(@types/node@22.7.6) packages: @@ -306,25 +306,25 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/node@22.7.5': - resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + '@types/node@22.7.6': + resolution: {integrity: sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==} - '@vitest/coverage-v8@2.1.2': - resolution: {integrity: sha512-b7kHrFrs2urS0cOk5N10lttI8UdJ/yP3nB4JYTREvR5o18cR99yPpK4gK8oQgI42BVv0ILWYUSYB7AXkAUDc0g==} + '@vitest/coverage-v8@2.1.3': + resolution: {integrity: sha512-2OJ3c7UPoFSmBZwqD2VEkUw6A/tzPF0LmW0ZZhhB8PFxuc+9IBG/FaSM+RLEenc7ljzFvGN+G0nGQoZnh7sy2A==} peerDependencies: - '@vitest/browser': 2.1.2 - vitest: 2.1.2 + '@vitest/browser': 2.1.3 + vitest: 2.1.3 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@2.1.2': - resolution: {integrity: sha512-FEgtlN8mIUSEAAnlvn7mP8vzaWhEaAEvhSXCqrsijM7K6QqjB11qoRZYEd4AKSCDz8p0/+yH5LzhZ47qt+EyPg==} + '@vitest/expect@2.1.3': + resolution: {integrity: sha512-SNBoPubeCJhZ48agjXruCI57DvxcsivVDdWz+SSsmjTT4QN/DfHk3zB/xKsJqMs26bLZ/pNRLnCf0j679i0uWQ==} - '@vitest/mocker@2.1.2': - resolution: {integrity: sha512-ExElkCGMS13JAJy+812fw1aCv2QO/LBK6CyO4WOPAzLTmve50gydOlWhgdBJPx2ztbADUq3JVI0C5U+bShaeEA==} + '@vitest/mocker@2.1.3': + resolution: {integrity: sha512-eSpdY/eJDuOvuTA3ASzCjdithHa+GIF1L4PqtEELl6Qa3XafdMLBpBlZCIUCX2J+Q6sNmjmxtosAG62fK4BlqQ==} peerDependencies: - '@vitest/spy': 2.1.2 + '@vitest/spy': 2.1.3 msw: ^2.3.5 vite: ^5.0.0 peerDependenciesMeta: @@ -333,20 +333,20 @@ packages: vite: optional: true - '@vitest/pretty-format@2.1.2': - resolution: {integrity: sha512-FIoglbHrSUlOJPDGIrh2bjX1sNars5HbxlcsFKCtKzu4+5lpsRhOCVcuzp0fEhAGHkPZRIXVNzPcpSlkoZ3LuA==} + '@vitest/pretty-format@2.1.3': + resolution: {integrity: sha512-XH1XdtoLZCpqV59KRbPrIhFCOO0hErxrQCMcvnQete3Vibb9UeIOX02uFPfVn3Z9ZXsq78etlfyhnkmIZSzIwQ==} - '@vitest/runner@2.1.2': - resolution: {integrity: sha512-UCsPtvluHO3u7jdoONGjOSil+uON5SSvU9buQh3lP7GgUXHp78guN1wRmZDX4wGK6J10f9NUtP6pO+SFquoMlw==} + '@vitest/runner@2.1.3': + resolution: {integrity: sha512-JGzpWqmFJ4fq5ZKHtVO3Xuy1iF2rHGV4d/pdzgkYHm1+gOzNZtqjvyiaDGJytRyMU54qkxpNzCx+PErzJ1/JqQ==} - '@vitest/snapshot@2.1.2': - resolution: {integrity: sha512-xtAeNsZ++aRIYIUsek7VHzry/9AcxeULlegBvsdLncLmNCR6tR8SRjn8BbDP4naxtccvzTqZ+L1ltZlRCfBZFA==} + '@vitest/snapshot@2.1.3': + resolution: {integrity: sha512-qWC2mWc7VAXmjAkEKxrScWHWFyCQx/cmiZtuGqMi+WwqQJ2iURsVY4ZfAK6dVo6K2smKRU6l3BPwqEBvhnpQGg==} - '@vitest/spy@2.1.2': - resolution: {integrity: sha512-GSUi5zoy+abNRJwmFhBDC0yRuVUn8WMlQscvnbbXdKLXX9dE59YbfwXxuJ/mth6eeqIzofU8BB5XDo/Ns/qK2A==} + '@vitest/spy@2.1.3': + resolution: {integrity: sha512-Nb2UzbcUswzeSP7JksMDaqsI43Sj5+Kry6ry6jQJT4b5gAK+NS9NED6mDb8FlMRCX8m5guaHCDZmqYMMWRy5nQ==} - '@vitest/utils@2.1.2': - resolution: {integrity: sha512-zMO2KdYy6mx56btx9JvAqAZ6EyS3g49krMPPrgOp1yxGZiA93HumGk+bZ5jIZtOg5/VBYl5eBmGRQHqq4FG6uQ==} + '@vitest/utils@2.1.3': + resolution: {integrity: sha512-xpiVfDSg1RrYT0tX6czgerkpcKFmFOF/gCr30+Mve5V2kewCy4Prn1/NDMSRwaSmT7PRaOF83wu+bEtsY1wrvA==} ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} @@ -523,8 +523,8 @@ packages: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} - picocolors@1.1.0: - resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} postcss@8.4.47: resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} @@ -592,8 +592,8 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@0.3.0: - resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==} + tinyexec@0.3.1: + resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} tinypool@1.0.1: resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==} @@ -619,13 +619,13 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - vite-node@2.1.2: - resolution: {integrity: sha512-HPcGNN5g/7I2OtPjLqgOtCRu/qhVvBxTUD3qzitmL0SrG1cWFzxzhMDWussxSbrRYWqnKf8P2jiNhPMSN+ymsQ==} + vite-node@2.1.3: + resolution: {integrity: sha512-I1JadzO+xYX887S39Do+paRePCKoiDrWRRjp9kkG5he0t7RXNvPAJPCQSJqbGN4uCrFFeS3Kj3sLqY8NMYBEdA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - vite@5.4.8: - resolution: {integrity: sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==} + vite@5.4.9: + resolution: {integrity: sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -655,15 +655,15 @@ packages: terser: optional: true - vitest@2.1.2: - resolution: {integrity: sha512-veNjLizOMkRrJ6xxb+pvxN6/QAWg95mzcRjtmkepXdN87FNfxAss9RKe2far/G9cQpipfgP2taqg0KiWsquj8A==} + vitest@2.1.3: + resolution: {integrity: sha512-Zrxbg/WiIvUP2uEzelDNTXmEMJXuzJ1kCpbDvaKByFA9MNeO95V+7r/3ti0qzJzrxdyuUw5VduN7k+D3VmVOSA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.1.2 - '@vitest/ui': 2.1.2 + '@vitest/browser': 2.1.3 + '@vitest/ui': 2.1.3 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -873,11 +873,11 @@ snapshots: '@types/estree@1.0.6': {} - '@types/node@22.7.5': + '@types/node@22.7.6': dependencies: undici-types: 6.19.8 - '@vitest/coverage-v8@2.1.2(vitest@2.1.2(@types/node@22.7.5))': + '@vitest/coverage-v8@2.1.3(vitest@2.1.3(@types/node@22.7.6))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -891,47 +891,47 @@ snapshots: std-env: 3.7.0 test-exclude: 7.0.1 tinyrainbow: 1.2.0 - vitest: 2.1.2(@types/node@22.7.5) + vitest: 2.1.3(@types/node@22.7.6) transitivePeerDependencies: - supports-color - '@vitest/expect@2.1.2': + '@vitest/expect@2.1.3': dependencies: - '@vitest/spy': 2.1.2 - '@vitest/utils': 2.1.2 + '@vitest/spy': 2.1.3 + '@vitest/utils': 2.1.3 chai: 5.1.1 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.7.5))': + '@vitest/mocker@2.1.3(@vitest/spy@2.1.3)(vite@5.4.9(@types/node@22.7.6))': dependencies: - '@vitest/spy': 2.1.2 + '@vitest/spy': 2.1.3 estree-walker: 3.0.3 magic-string: 0.30.12 optionalDependencies: - vite: 5.4.8(@types/node@22.7.5) + vite: 5.4.9(@types/node@22.7.6) - '@vitest/pretty-format@2.1.2': + '@vitest/pretty-format@2.1.3': dependencies: tinyrainbow: 1.2.0 - '@vitest/runner@2.1.2': + '@vitest/runner@2.1.3': dependencies: - '@vitest/utils': 2.1.2 + '@vitest/utils': 2.1.3 pathe: 1.1.2 - '@vitest/snapshot@2.1.2': + '@vitest/snapshot@2.1.3': dependencies: - '@vitest/pretty-format': 2.1.2 + '@vitest/pretty-format': 2.1.3 magic-string: 0.30.12 pathe: 1.1.2 - '@vitest/spy@2.1.2': + '@vitest/spy@2.1.3': dependencies: tinyspy: 3.0.2 - '@vitest/utils@2.1.2': + '@vitest/utils@2.1.3': dependencies: - '@vitest/pretty-format': 2.1.2 + '@vitest/pretty-format': 2.1.3 loupe: 3.1.2 tinyrainbow: 1.2.0 @@ -1112,12 +1112,12 @@ snapshots: pathval@2.0.0: {} - picocolors@1.1.0: {} + picocolors@1.1.1: {} postcss@8.4.47: dependencies: nanoid: 3.3.7 - picocolors: 1.1.0 + picocolors: 1.1.1 source-map-js: 1.2.1 rollup@4.24.0: @@ -1192,7 +1192,7 @@ snapshots: tinybench@2.9.0: {} - tinyexec@0.3.0: {} + tinyexec@0.3.1: {} tinypool@1.0.1: {} @@ -1206,12 +1206,12 @@ snapshots: undici-types@6.19.8: {} - vite-node@2.1.2(@types/node@22.7.5): + vite-node@2.1.3(@types/node@22.7.6): dependencies: cac: 6.7.14 debug: 4.3.7 pathe: 1.1.2 - vite: 5.4.8(@types/node@22.7.5) + vite: 5.4.9(@types/node@22.7.6) transitivePeerDependencies: - '@types/node' - less @@ -1223,38 +1223,38 @@ snapshots: - supports-color - terser - vite@5.4.8(@types/node@22.7.5): + vite@5.4.9(@types/node@22.7.6): dependencies: esbuild: 0.21.5 postcss: 8.4.47 rollup: 4.24.0 optionalDependencies: - '@types/node': 22.7.5 + '@types/node': 22.7.6 fsevents: 2.3.3 - vitest@2.1.2(@types/node@22.7.5): + vitest@2.1.3(@types/node@22.7.6): dependencies: - '@vitest/expect': 2.1.2 - '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.7.5)) - '@vitest/pretty-format': 2.1.2 - '@vitest/runner': 2.1.2 - '@vitest/snapshot': 2.1.2 - '@vitest/spy': 2.1.2 - '@vitest/utils': 2.1.2 + '@vitest/expect': 2.1.3 + '@vitest/mocker': 2.1.3(@vitest/spy@2.1.3)(vite@5.4.9(@types/node@22.7.6)) + '@vitest/pretty-format': 2.1.3 + '@vitest/runner': 2.1.3 + '@vitest/snapshot': 2.1.3 + '@vitest/spy': 2.1.3 + '@vitest/utils': 2.1.3 chai: 5.1.1 debug: 4.3.7 magic-string: 0.30.12 pathe: 1.1.2 std-env: 3.7.0 tinybench: 2.9.0 - tinyexec: 0.3.0 + tinyexec: 0.3.1 tinypool: 1.0.1 tinyrainbow: 1.2.0 - vite: 5.4.8(@types/node@22.7.5) - vite-node: 2.1.2(@types/node@22.7.5) + vite: 5.4.9(@types/node@22.7.6) + vite-node: 2.1.3(@types/node@22.7.6) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.7.5 + '@types/node': 22.7.6 transitivePeerDependencies: - less - lightningcss diff --git a/src/_node/compat.ts b/src/_node/compat.ts new file mode 100644 index 0000000..18a7a22 --- /dev/null +++ b/src/_node/compat.ts @@ -0,0 +1,58 @@ +import { Cipher, concatBytes } from "@noble/ciphers/utils"; +import { CipherGCM, createCipheriv, createDecipheriv, DecipherGCM } from "node:crypto"; + +const AEAD_TAG_LENGTH = 16; + +/** + * make `node:crypto`'s ciphers compatible with `@noble/ciphers`. + * + * `Cipher`'s interface is the same for both `aes-256-gcm` and `chacha20-poly1305`, + * albeit the latter is one of `CipherCCMTypes`. + * Interestingly, whether to set `plaintextLength` or not, or which value to set, has no actual effect. + */ +export const _compat = ( + algorithm: "aes-256-gcm" | "aes-256-cbc" | "chacha20-poly1305", + key: Uint8Array, + nonce: Uint8Array, + AAD?: Uint8Array +): Cipher => { + const isAEAD = algorithm === "aes-256-gcm" || algorithm === "chacha20-poly1305"; + const authTagLength = isAEAD ? AEAD_TAG_LENGTH : 0; + // authTagLength is necessary for `chacha20-poly1305` before Node v16.17 + const options = isAEAD ? { authTagLength } : undefined; + + const encrypt = (plainText: Uint8Array) => { + const cipher = createCipheriv(algorithm, key, nonce, options as any); + if (isAEAD && AAD !== undefined) { + (cipher as CipherGCM).setAAD(AAD); + } + + const updated = cipher.update(plainText); + const finalized = cipher.final(); + if (isAEAD) { + return concatBytes(updated, finalized, (cipher as CipherGCM).getAuthTag()); + } + return concatBytes(updated, finalized); + }; + + const decrypt = (cipherText: Uint8Array) => { + const rawCipherText = cipherText.subarray(0, cipherText.length - authTagLength); + const tag = cipherText.subarray(cipherText.length - authTagLength); + + const decipher = createDecipheriv(algorithm, key, nonce, options as any); + if (isAEAD) { + if (AAD !== undefined) { + (decipher as DecipherGCM).setAAD(AAD); + } + (decipher as DecipherGCM).setAuthTag(tag); + } + const updated = decipher.update(rawCipherText); + const finalized = decipher.final(); + return concatBytes(updated, finalized); + }; + + return { + encrypt, + decrypt, + }; +}; diff --git a/src/_node/hchacha.ts b/src/_node/hchacha.ts new file mode 100644 index 0000000..f5a195c --- /dev/null +++ b/src/_node/hchacha.ts @@ -0,0 +1,63 @@ +/** + * Copied from `@noble/ciphers/chacha` + */ +// prettier-ignore +export const _hchacha = ( + s: Uint32Array, k: Uint32Array, i: Uint32Array, o32: Uint32Array +): void => { + let x00 = s[0], x01 = s[1], x02 = s[2], x03 = s[3], + x04 = k[0], x05 = k[1], x06 = k[2], x07 = k[3], + x08 = k[4], x09 = k[5], x10 = k[6], x11 = k[7], + x12 = i[0], x13 = i[1], x14 = i[2], x15 = i[3]; + + for (let r = 0; r < 20; r += 2) { + x00 = (x00 + x04) | 0; x12 = rotl(x12 ^ x00, 16); + x08 = (x08 + x12) | 0; x04 = rotl(x04 ^ x08, 12); + x00 = (x00 + x04) | 0; x12 = rotl(x12 ^ x00, 8); + x08 = (x08 + x12) | 0; x04 = rotl(x04 ^ x08, 7); + + x01 = (x01 + x05) | 0; x13 = rotl(x13 ^ x01, 16); + x09 = (x09 + x13) | 0; x05 = rotl(x05 ^ x09, 12); + x01 = (x01 + x05) | 0; x13 = rotl(x13 ^ x01, 8); + x09 = (x09 + x13) | 0; x05 = rotl(x05 ^ x09, 7); + + x02 = (x02 + x06) | 0; x14 = rotl(x14 ^ x02, 16); + x10 = (x10 + x14) | 0; x06 = rotl(x06 ^ x10, 12); + x02 = (x02 + x06) | 0; x14 = rotl(x14 ^ x02, 8); + x10 = (x10 + x14) | 0; x06 = rotl(x06 ^ x10, 7); + + x03 = (x03 + x07) | 0; x15 = rotl(x15 ^ x03, 16); + x11 = (x11 + x15) | 0; x07 = rotl(x07 ^ x11, 12); + x03 = (x03 + x07) | 0; x15 = rotl(x15 ^ x03, 8) + x11 = (x11 + x15) | 0; x07 = rotl(x07 ^ x11, 7); + + x00 = (x00 + x05) | 0; x15 = rotl(x15 ^ x00, 16); + x10 = (x10 + x15) | 0; x05 = rotl(x05 ^ x10, 12); + x00 = (x00 + x05) | 0; x15 = rotl(x15 ^ x00, 8); + x10 = (x10 + x15) | 0; x05 = rotl(x05 ^ x10, 7); + + x01 = (x01 + x06) | 0; x12 = rotl(x12 ^ x01, 16); + x11 = (x11 + x12) | 0; x06 = rotl(x06 ^ x11, 12); + x01 = (x01 + x06) | 0; x12 = rotl(x12 ^ x01, 8); + x11 = (x11 + x12) | 0; x06 = rotl(x06 ^ x11, 7); + + x02 = (x02 + x07) | 0; x13 = rotl(x13 ^ x02, 16); + x08 = (x08 + x13) | 0; x07 = rotl(x07 ^ x08, 12); + x02 = (x02 + x07) | 0; x13 = rotl(x13 ^ x02, 8); + x08 = (x08 + x13) | 0; x07 = rotl(x07 ^ x08, 7); + + x03 = (x03 + x04) | 0; x14 = rotl(x14 ^ x03, 16) + x09 = (x09 + x14) | 0; x04 = rotl(x04 ^ x09, 12); + x03 = (x03 + x04) | 0; x14 = rotl(x14 ^ x03, 8); + x09 = (x09 + x14) | 0; x04 = rotl(x04 ^ x09, 7); + } + let oi = 0; + o32[oi++] = x00; o32[oi++] = x01; + o32[oi++] = x02; o32[oi++] = x03; + o32[oi++] = x12; o32[oi++] = x13; + o32[oi++] = x14; o32[oi++] = x15; +} + +const rotl = (a: number, b: number): number => { + return (a << b) | (a >>> (32 - b)); +}; diff --git a/src/noble.ts b/src/aes/noble.ts similarity index 100% rename from src/noble.ts rename to src/aes/noble.ts diff --git a/src/aes/node.ts b/src/aes/node.ts new file mode 100644 index 0000000..51fd28e --- /dev/null +++ b/src/aes/node.ts @@ -0,0 +1,9 @@ +import { Cipher } from "@noble/ciphers/utils"; + +import { _compat } from "../_node/compat"; + +export const aes256gcm = (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array): Cipher => + _compat("aes-256-gcm", key, nonce, AAD); + +export const aes256cbc = (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array): Cipher => + _compat("aes-256-cbc", key, nonce); diff --git a/src/chacha/noble.ts b/src/chacha/noble.ts new file mode 100644 index 0000000..606785b --- /dev/null +++ b/src/chacha/noble.ts @@ -0,0 +1,5 @@ +import { xchacha20poly1305 } from "@noble/ciphers/chacha"; +import { Cipher } from "@noble/ciphers/utils"; + +export const xchacha20 = (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array): Cipher => + xchacha20poly1305(key, nonce, AAD); diff --git a/src/chacha/node.ts b/src/chacha/node.ts new file mode 100644 index 0000000..50975f1 --- /dev/null +++ b/src/chacha/node.ts @@ -0,0 +1,23 @@ +import { Cipher, u32, u8 } from "@noble/ciphers/utils"; + +import { _compat } from "../_node/compat"; +import { _hchacha } from "../_node/hchacha"; + +export const xchacha20 = ( + key: Uint8Array, + nonce: Uint8Array, + AAD?: Uint8Array +): Cipher => { + if (nonce.length !== 24) { + throw new Error("xchacha20's nonce must be 24 bytes"); + } + const constants = new Uint32Array([0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]); // "expand 32-byte k" + const subKey = new Uint32Array(8); + + _hchacha(constants, u32(key), u32(nonce.subarray(0, 16)), subKey); + + const subNonce = new Uint8Array(12); + subNonce.set([0, 0, 0, 0]); + subNonce.set(nonce.subarray(16), 4); + return _compat("chacha20-poly1305", u8(subKey), subNonce, AAD); +}; diff --git a/src/node.ts b/src/node.ts deleted file mode 100644 index 63c55b8..0000000 --- a/src/node.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Cipher, concatBytes } from "@noble/ciphers/utils"; -import { CipherGCM, createCipheriv, createDecipheriv, DecipherGCM } from "node:crypto"; - -const AEAD_TAG_LENGTH = 16; - -// make `node:crypto`'s aes compatible with `@noble/ciphers` -const _compat = ( - algorithm: "aes-256-gcm" | "aes-256-cbc", - key: Uint8Array, - nonce: Uint8Array, - AAD?: Uint8Array -): Cipher => { - const isAEAD = algorithm === "aes-256-gcm"; - const tagLength = isAEAD ? AEAD_TAG_LENGTH : 0; - - const encrypt = (plainText: Uint8Array) => { - const cipher = createCipheriv(algorithm, key, nonce); - if (isAEAD && AAD) { - (cipher as CipherGCM).setAAD(AAD); - } - - const updated = cipher.update(plainText); - const finalized = cipher.final(); - if (isAEAD) { - return concatBytes(updated, finalized, (cipher as CipherGCM).getAuthTag()); - } - return concatBytes(updated, finalized); - }; - - const decrypt = (cipherText: Uint8Array) => { - const encrypted = cipherText.subarray(0, cipherText.length - tagLength); - const tag = cipherText.subarray(cipherText.length - tagLength); - - const decipher = createDecipheriv(algorithm, key, nonce); - if (isAEAD) { - if (AAD) { - (decipher as DecipherGCM).setAAD(AAD); - } - (decipher as DecipherGCM).setAuthTag(tag); - } - const updated = decipher.update(encrypted); - const finalized = decipher.final(); - return concatBytes(updated, finalized); - }; - - return { - encrypt, - decrypt, - }; -}; - -export const aes256gcm = (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array): Cipher => - _compat("aes-256-gcm", key, nonce, AAD); - -export const aes256cbc = (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array): Cipher => - _compat("aes-256-cbc", key, nonce); diff --git a/tests/aes/known.test.ts b/tests/aes/known.test.ts new file mode 100644 index 0000000..d91ac8e --- /dev/null +++ b/tests/aes/known.test.ts @@ -0,0 +1,42 @@ +import { describe, it } from "vitest"; + +import { concatBytes, hexToBytes } from "@noble/ciphers/utils"; + +import { aes256cbc as _aes256cbc, aes256gcm as _aes256gcm } from "../../src/aes/noble"; +import { aes256cbc, aes256gcm } from "../../src/aes/node"; +import { testKnown } from "../common"; + +// from https://github.com/C2SP/wycheproof/tree/master/testvectors +describe("test known", () => { + it("tests gcm", () => { + const key = hexToBytes( + "92ace3e348cd821092cd921aa3546374299ab46209691bc28b8752d17f123c20" + ); + const nonce = hexToBytes("00112233445566778899aabb"); + const aad = hexToBytes("00000000ffffffff"); + const noble = _aes256gcm(key, nonce, aad); + const compat = aes256gcm(key, nonce, aad); + + const cipherText = hexToBytes("e27abdd2d2a53d2f136b"); + const tag = hexToBytes("9a4a2579529301bcfb71c78d4060f52c"); + const encrypted = concatBytes(cipherText, tag); + + const plainText = hexToBytes("00010203040506070809"); + + testKnown(plainText, encrypted, noble, compat); + }); + + it("tests cbc", () => { + const key = hexToBytes( + "7bf9e536b66a215c22233fe2daaa743a898b9acb9f7802de70b40e3d6e43ef97" + ); + const nonce = hexToBytes("eb38ef61717e1324ae064e86f1c3e797"); + const noble = _aes256cbc(key, nonce); + const compat = aes256cbc(key, nonce); + + const cipherText = hexToBytes("e7c166554d1bb32792c981fa674cc4d8"); + const plainText = Uint8Array.from([]); + + testKnown(plainText, cipherText, noble, compat); + }); +}); diff --git a/tests/aes/random.test.ts b/tests/aes/random.test.ts new file mode 100644 index 0000000..c446397 --- /dev/null +++ b/tests/aes/random.test.ts @@ -0,0 +1,39 @@ +import { describe, it } from "vitest"; + +import { randomBytes } from "@noble/ciphers/webcrypto"; + +import { aes256cbc as _aes256cbc, aes256gcm as _aes256gcm } from "../../src/aes/noble"; +import { aes256cbc, aes256gcm } from "../../src/aes/node"; +import { hello, testRandom } from "../common"; + +describe("test random", () => { + function testGcm(nonceLength: number, aad?: Uint8Array) { + const key = randomBytes(); + const nonce = randomBytes(nonceLength); + const noble = _aes256gcm(key, nonce, aad); + const compat = aes256gcm(key, nonce, aad); + testRandom(hello, noble, compat); + } + + function testCbc() { + const key = randomBytes(); + const nonce = randomBytes(16); + const noble = _aes256cbc(key, nonce); + const compat = aes256cbc(key, nonce); + testRandom(hello, noble, compat); + } + + it("tests gcm", () => { + testGcm(16); + testGcm(16, randomBytes(8)); + testGcm(16, randomBytes(16)); + + testGcm(12); + testGcm(12, randomBytes(8)); + testGcm(12, randomBytes(16)); + }); + + it("tests cbc", () => { + testCbc(); + }); +}); diff --git a/tests/chacha/known.test.ts b/tests/chacha/known.test.ts new file mode 100644 index 0000000..6389fc3 --- /dev/null +++ b/tests/chacha/known.test.ts @@ -0,0 +1,32 @@ +import { describe, it } from "vitest"; + +import { concatBytes, hexToBytes } from "@noble/ciphers/utils"; + +import { xchacha20 as _xchacha20 } from "../../src/chacha/noble"; +import { xchacha20 } from "../../src/chacha/node"; +import { testKnown } from "../common"; + +describe("test known", () => { + it("tests xchacha20", () => { + // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-01#appendix-A.3.1 + const key = hexToBytes( + "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f" + ); + const nonce = hexToBytes("404142434445464748494a4b4c4d4e4f5051525354555657"); + const aad = hexToBytes("50515253c0c1c2c3c4c5c6c7"); + const noble = _xchacha20(key, nonce, aad); + const compat = xchacha20(key, nonce, aad); + + const cipherText = hexToBytes( + "bd6d179d3e83d43b9576579493c0e939572a1700252bfaccbed2902c21396cbb731c7f1b0b4aa6440bf3a82f4eda7e39ae64c6708c54c216cb96b72e1213b4522f8c9ba40db5d945b11b69b982c1bb9e3f3fac2bc369488f76b2383565d3fff921f9664c97637da9768812f615c68b13b52e" + ); + const tag = hexToBytes("c0875924c1c7987947deafd8780acf49"); + const encrypted = concatBytes(cipherText, tag); + + const plainText = hexToBytes( + "4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e20776f756c642062652069742e" + ); + + testKnown(plainText, encrypted, noble, compat); + }); +}); diff --git a/tests/chacha/random.test.ts b/tests/chacha/random.test.ts new file mode 100644 index 0000000..d9af106 --- /dev/null +++ b/tests/chacha/random.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; + +import { randomBytes } from "@noble/ciphers/webcrypto"; + +import { xchacha20 as _xchacha20 } from "../../src/chacha/noble"; +import { xchacha20 } from "../../src/chacha/node"; +import { hello, testRandom } from "../common"; + +describe("test random", () => { + function testXChacha20(aad?: Uint8Array) { + const key = randomBytes(); + const nonce = randomBytes(24); + const noble = _xchacha20(key, nonce, aad); + const compat = xchacha20(key, nonce, aad); + testRandom(hello, noble, compat); + } + + function testXChacha20Error() { + const key = randomBytes(); + expect(() => xchacha20(key, randomBytes(12))).toThrowError( + "xchacha20's nonce must be 24 bytes" + ); + } + + it("tests xchacha20", () => { + testXChacha20(); + testXChacha20(randomBytes(8)); + testXChacha20(randomBytes(16)); + testXChacha20Error(); + }); +}); diff --git a/tests/common.ts b/tests/common.ts new file mode 100644 index 0000000..56dc94f --- /dev/null +++ b/tests/common.ts @@ -0,0 +1,30 @@ +import { expect } from "vitest"; + +import { Cipher } from "@noble/ciphers/utils"; + +const TEXT = "hello world🌍!"; +const encoder = new TextEncoder(); +export const hello = encoder.encode(TEXT); + +export function testRandom(data: Uint8Array, noble: Cipher, compat: Cipher) { + // same encryption + expect(noble.encrypt(data)).toStrictEqual(compat.encrypt(data)); + // noble encrypts, compat decrypts + expect(compat.decrypt(noble.encrypt(data))).toStrictEqual(data); + // noble decrypts, compat encrypts + expect(noble.decrypt(compat.encrypt(data))).toStrictEqual(data); +} + +export function testKnown( + data: Uint8Array, + encrypted: Uint8Array, + noble: Cipher, + compat: Cipher +) { + // same encryption + expect(compat.encrypt(data)).toStrictEqual(encrypted); + expect(noble.encrypt(data)).toStrictEqual(encrypted); + // same decryption + expect(compat.decrypt(encrypted)).toStrictEqual(data); + expect(noble.decrypt(encrypted)).toStrictEqual(data); +} diff --git a/tests/known.test.ts b/tests/known.test.ts deleted file mode 100644 index d57116c..0000000 --- a/tests/known.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { concatBytes, hexToBytes } from "@noble/ciphers/utils"; - -import { aes256gcm as _aes256gcm } from "../src/noble"; -import { aes256gcm } from "../src/node"; - -const encoder = new TextEncoder(); - -describe("test known", () => { - it("tests gcm", async () => { - const key = hexToBytes( - "0000000000000000000000000000000000000000000000000000000000000000" - ); - const nonce = hexToBytes("f3e1ba810d2c8900b11312b7c725565f"); - const tag = hexToBytes("ec3b71e17c11dbe31484da9450edcf6c"); - const encrypted = hexToBytes("02d2ffed93b856f148b9"); - const known = concatBytes(encrypted, tag); - const msg = encoder.encode("helloworld"); - const aad = Uint8Array.from([]); - - const noble = _aes256gcm(key, nonce, aad); - const compat = aes256gcm(key, nonce, aad); - expect(compat.decrypt(known)).toStrictEqual(msg); - expect(noble.decrypt(known)).toStrictEqual(msg); - }); -}); diff --git a/tests/random.test.ts b/tests/random.test.ts deleted file mode 100644 index ec89e26..0000000 --- a/tests/random.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { Cipher } from "@noble/ciphers/utils"; -import { randomBytes } from "@noble/ciphers/webcrypto"; - -import { aes256cbc as _aes256cbc, aes256gcm as _aes256gcm } from "../src/noble"; -import { aes256cbc, aes256gcm } from "../src/node"; - -const TEXT = "helloworld🌍"; -const encoder = new TextEncoder(); - -describe("test random", () => { - const msg = encoder.encode(TEXT); - - function testCipher(noble: Cipher, compat: Cipher) { - // same encryption - expect(noble.encrypt(msg)).toStrictEqual(compat.encrypt(msg)); - // noble encrypts, compat decrypts - expect(compat.decrypt(noble.encrypt(msg))).toStrictEqual(msg); - // noble decrypts, compat encrypts - expect(noble.decrypt(compat.encrypt(msg))).toStrictEqual(msg); - } - - function testGcm(aad?: Uint8Array) { - const key = randomBytes(); - const nonce = randomBytes(16); - const noble = _aes256gcm(key, nonce, aad); - const compat = aes256gcm(key, nonce, aad); - testCipher(noble, compat); - } - - function testCbc() { - const key = randomBytes(); - const nonce = randomBytes(16); - const noble = _aes256cbc(key, nonce); - const compat = aes256cbc(key, nonce); - testCipher(noble, compat); - } - - it("tests gcm", () => { - testGcm(); - testGcm(randomBytes(8)); - testGcm(randomBytes(16)); - }); - - it("tests cbc", () => { - testCbc(); - }); -});