diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ae8655de2..dc096eb4f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -43,6 +43,15 @@ jobs: - name: Install deps run: npm install + - name: Build Twofish WASM + working-directory: packages/twofish + run: | + DOCKER_UID=$(id -u) + DOCKER_GID=$(id -g) + export DOCKER_UID + export DOCKER_GID + npm run build + - name: Linter run: npm run lint diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..4617fe0a6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Contributing + +## Prerequisites + +| Tool | Version | Purpose | +|------|---------|---------| +| Node.js | 22+ | Runtime & build tooling | +| Docker | any recent | Building the Twofish WASM module | + +## Setup + +```bash +git clone +cd +npm install +``` + +## Building + +Build everything (WASM + TypeScript): + +Build dependencies: + +```bash +cd packages/twofish +npm run build +``` + +The WASM build runs inside Docker automatically — no need to install `clang` or `lld` locally. Docker must be running. + +Then build the app: + +```bash +npm run build +``` + +## Linting & Tests + +```bash +npm run lint +npm test +``` + +## CI + +GitHub Actions runs on every push to `master` and on pull requests: + +- **actionlint** — validates GitHub Actions workflow files +- **tests** — builds, lints, and runs the test suite +- **CodeQL** — static security analysis + +PRs from forks skip the CodeQL step. \ No newline at end of file diff --git a/eslint.config.ts b/eslint.config.ts index 6fc9abc2f..4f6e5ad51 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -275,4 +275,16 @@ export default tseslint.config( ], }, }, + + // Nested packages + { + files: ['packages/twofish/**/*.ts'], + languageOptions: { + parserOptions: { + projectService: { + defaultProject: 'packages/twofish/tsconfig.json', + }, + }, + }, + }, ); diff --git a/package-lock.json b/package-lock.json index 11c8423cb..b989f0b56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": " AGPL-3.0", "workspaces": [ - "packages/site" + "packages/*" ], "dependencies": { "@chakra-ui/react": "^2.10.9", @@ -47,7 +47,9 @@ "fuzzysort": "^3.1.0", "hotkeys-js": "^4.0.0", "humanize-duration": "^3.33.2", + "leviathan-crypto": "^1.4.0", "lexical": "^0.39.0", + "libsodium-wrappers-sumo": "^0.8.2", "lodash": "^4.17.23", "lru-cache": "^11.2.7", "monaco-editor-core": "^0.55.1", @@ -77,7 +79,7 @@ "signal-exit": "^4.1.0", "sql.js": "^1.14.1", "systeminformation": "^5.30.6", - "twofish-ts": "^1.0.2", + "twofish": "^0.0.0", "unified": "^11.0.5", "unist-builder": "^4.0.0", "unist-util-remove": "^4.0.0", @@ -8189,6 +8191,16 @@ "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", "license": "MIT" }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@pagefind/darwin-arm64": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.4.0.tgz", @@ -8312,6 +8324,19 @@ "url": "https://opencollective.com/preact" } }, + "node_modules/@quansync/fs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz", + "integrity": "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", @@ -8369,6 +8394,278 @@ "url": "https://github.com/SpacingBat3/ReForged?sponsor=1" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -9810,6 +10107,13 @@ "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", "license": "MIT" }, + "node_modules/@types/jsesc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", + "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -9897,12 +10201,12 @@ } }, "node_modules/@types/node": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.1.0.tgz", - "integrity": "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==", + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/parse-json": { @@ -12735,6 +13039,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -13025,6 +13339,74 @@ "node": ">=12" } }, + "node_modules/ast-kit": { + "version": "3.0.0-beta.1", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-3.0.0-beta.1.tgz", + "integrity": "sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^8.0.0-beta.4", + "estree-walker": "^3.0.3", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/ast-kit/node_modules/@babel/helper-string-parser": { + "version": "8.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.3.tgz", + "integrity": "sha512-AmwWFx1m8G/a5cXkxLxTiWl+YEoWuoFLUCwqMlNuWO1tqAYITQAbCRPUkyBHv1VOFgfjVOqEj6L3u15J5ZCzTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/ast-kit/node_modules/@babel/helper-validator-identifier": { + "version": "8.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.3.tgz", + "integrity": "sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/ast-kit/node_modules/@babel/parser": { + "version": "8.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.3.tgz", + "integrity": "sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.0-rc.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/ast-kit/node_modules/@babel/types": { + "version": "8.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.3.tgz", + "integrity": "sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^8.0.0-rc.3", + "@babel/helper-validator-identifier": "^8.0.0-rc.3" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -14096,6 +14478,16 @@ "node": "*" } }, + "node_modules/birpc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-4.0.0.tgz", + "integrity": "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -14396,6 +14788,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/cac": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", + "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/cacache": { "version": "16.1.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", @@ -16432,6 +16834,27 @@ "node": ">=4" } }, + "node_modules/dts-resolver": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/dts-resolver/-/dts-resolver-2.1.3.tgz", + "integrity": "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "oxc-resolver": ">=11.0.0" + }, + "peerDependenciesMeta": { + "oxc-resolver": { + "optional": true + } + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -16981,6 +17404,16 @@ "node": ">= 4" } }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/encode-utf8": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", @@ -19374,9 +19807,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", "devOptional": true, "license": "MIT", "dependencies": { @@ -20293,6 +20726,13 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/hookable": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.0.tgz", + "integrity": "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==", + "dev": true, + "license": "MIT" + }, "node_modules/hookified": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.0.tgz", @@ -20759,6 +21199,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/import-without-cache": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/import-without-cache/-/import-without-cache-0.2.5.tgz", + "integrity": "sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/imul": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/imul/-/imul-1.0.1.tgz", @@ -22031,6 +22484,12 @@ "dev": true, "license": "MIT" }, + "node_modules/leviathan-crypto": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/leviathan-crypto/-/leviathan-crypto-1.4.0.tgz", + "integrity": "sha512-cFOKvekT24ew3p6lYzVqGeV3DQRKrSyTI42Ft1GO7WuwAemvzDZECb3yDe+eOngFvyWcN7PMEJ27Kvroa2ttHw==", + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -22073,6 +22532,21 @@ "url": "https://github.com/sponsors/dmonad" } }, + "node_modules/libsodium-sumo": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.8.2.tgz", + "integrity": "sha512-uMgnjphJ717jLN+jFG1HUgNrK/gOVVfaO1DGZ1Ig/fKLKLVhvaH/sM1I1v784JFvmkJDaczDpi7xSYC4Jvdo1Q==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers-sumo": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.8.2.tgz", + "integrity": "sha512-wd1xAY++Kr6VMikSaa4EPRAHJmFvNlGWiiwU3Jh3GR1zRYF3/I3vy/wYsr4k3LVsNzwb9sqfEQ4LdVQ6zEebyQ==", + "license": "ISC", + "dependencies": { + "libsodium-sumo": "^0.8.0" + } + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -25947,9 +26421,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -26547,6 +27021,23 @@ "node": ">=20" } }, + "node_modules/quansync": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz", + "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -28013,6 +28504,160 @@ "node": ">=8.0" } }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, + "node_modules/rolldown-plugin-dts": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/rolldown-plugin-dts/-/rolldown-plugin-dts-0.23.1.tgz", + "integrity": "sha512-VTnvu5cksnumMMOiL7FUvACGpdGtCVNGbeVc6/6KffImIrA0DZOp7/0lBIt0qI6Nu3/K/lL/Dy7piuVGt9ZeGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "8.0.0-rc.3", + "@babel/helper-validator-identifier": "8.0.0-rc.3", + "@babel/parser": "8.0.0-rc.3", + "@babel/types": "8.0.0-rc.3", + "ast-kit": "^3.0.0-beta.1", + "birpc": "^4.0.0", + "dts-resolver": "^2.1.3", + "get-tsconfig": "^4.13.7", + "obug": "^2.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@ts-macro/tsc": "^0.3.6", + "@typescript/native-preview": ">=7.0.0-dev.20260325.1", + "rolldown": "^1.0.0-rc.12", + "typescript": "^5.0.0 || ^6.0.0", + "vue-tsc": "~3.2.0" + }, + "peerDependenciesMeta": { + "@ts-macro/tsc": { + "optional": true + }, + "@typescript/native-preview": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/rolldown-plugin-dts/node_modules/@babel/generator": { + "version": "8.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0-rc.3.tgz", + "integrity": "sha512-em37/13/nR320G4jab/nIIHZgc2Wz2y/D39lxnTyxB4/D/omPQncl/lSdlnJY1OhQcRGugTSIF2l/69o31C9dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^8.0.0-rc.3", + "@babel/types": "^8.0.0-rc.3", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "@types/jsesc": "^2.5.0", + "jsesc": "^3.0.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/rolldown-plugin-dts/node_modules/@babel/helper-string-parser": { + "version": "8.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.3.tgz", + "integrity": "sha512-AmwWFx1m8G/a5cXkxLxTiWl+YEoWuoFLUCwqMlNuWO1tqAYITQAbCRPUkyBHv1VOFgfjVOqEj6L3u15J5ZCzTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/rolldown-plugin-dts/node_modules/@babel/helper-validator-identifier": { + "version": "8.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.3.tgz", + "integrity": "sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/rolldown-plugin-dts/node_modules/@babel/parser": { + "version": "8.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.3.tgz", + "integrity": "sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.0-rc.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/rolldown-plugin-dts/node_modules/@babel/types": { + "version": "8.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.3.tgz", + "integrity": "sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^8.0.0-rc.3", + "@babel/helper-validator-identifier": "^8.0.0-rc.3" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", @@ -29989,9 +30634,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", "license": "MIT", "engines": { "node": ">=18" @@ -30160,6 +30805,16 @@ "tslib": "2" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -30380,6 +31035,72 @@ "node": ">=8" } }, + "node_modules/tsdown": { + "version": "0.21.6", + "resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.21.6.tgz", + "integrity": "sha512-YsgPuWczqxPkXiJwMPrv3eOiqx4KPhOdksqubVCDhS7lChK3RYlWsEGhZixc0+lqN3fmBYEnETaujEWDpMPZmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.2.0", + "cac": "^7.0.0", + "defu": "^6.1.4", + "empathic": "^2.0.0", + "hookable": "^6.1.0", + "import-without-cache": "^0.2.5", + "obug": "^2.1.1", + "picomatch": "^4.0.4", + "rolldown": "1.0.0-rc.12", + "rolldown-plugin-dts": "^0.23.0", + "semver": "^7.7.4", + "tinyexec": "^1.0.4", + "tinyglobby": "^0.2.15", + "tree-kill": "^1.2.2", + "unconfig-core": "^7.5.0", + "unrun": "^0.2.34" + }, + "bin": { + "tsdown": "dist/run.mjs" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@arethetypeswrong/core": "^0.18.1", + "@tsdown/css": "0.21.6", + "@tsdown/exe": "0.21.6", + "@vitejs/devtools": "*", + "publint": "^0.3.0", + "typescript": "^5.0.0 || ^6.0.0", + "unplugin-unused": "^0.5.0" + }, + "peerDependenciesMeta": { + "@arethetypeswrong/core": { + "optional": true + }, + "@tsdown/css": { + "optional": true + }, + "@tsdown/exe": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "publint": { + "optional": true + }, + "typescript": { + "optional": true + }, + "unplugin-unused": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -30406,11 +31127,9 @@ "fsevents": "~2.3.3" } }, - "node_modules/twofish-ts": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/twofish-ts/-/twofish-ts-1.0.2.tgz", - "integrity": "sha512-ogx3C6CE/zviqpFApfixBIqxzfF0P8lO1e0ep0w7squTLmGMnbYx/s7CutQCmRxQO68XUvY4iseeQ09rNITYOA==", - "license": "MIT" + "node_modules/twofish": { + "resolved": "packages/twofish", + "link": true }, "node_modules/type-check": { "version": "0.4.0", @@ -30620,6 +31339,20 @@ "ieee754": "^1.1.13" } }, + "node_modules/unconfig-core": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.5.0.tgz", + "integrity": "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quansync/fs": "^1.0.0", + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/uncrypto": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", @@ -30627,9 +31360,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -31027,6 +31760,33 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, + "node_modules/unrun": { + "version": "0.2.34", + "resolved": "https://registry.npmjs.org/unrun/-/unrun-0.2.34.tgz", + "integrity": "sha512-LyaghRBR++r7svhDK6tnDz2XaYHWdneBOA0jbS8wnRsHerI9MFljX4fIiTgbbNbEVzZ0C9P1OjWLLe1OqoaaEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "rolldown": "1.0.0-rc.12" + }, + "bin": { + "unrun": "dist/cli.mjs" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/Gugustinette" + }, + "peerDependencies": { + "synckit": "^0.11.11" + }, + "peerDependenciesMeta": { + "synckit": { + "optional": true + } + } + }, "node_modules/unstorage": { "version": "1.17.4", "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.4.tgz", @@ -32628,6 +33388,29 @@ "optional": true } } + }, + "packages/twofish": { + "version": "0.0.0", + "license": "MIT", + "devDependencies": { + "@types/node": "^25.5.0", + "tsdown": "^0.21.6", + "typescript": "^6.0.2" + } + }, + "packages/twofish/node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } } } } diff --git a/package.json b/package.json index 7ff5037f7..f1feb9527 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,9 @@ "fuzzysort": "^3.1.0", "hotkeys-js": "^4.0.0", "humanize-duration": "^3.33.2", + "leviathan-crypto": "^1.4.0", "lexical": "^0.39.0", + "libsodium-wrappers-sumo": "^0.8.2", "lodash": "^4.17.23", "lru-cache": "^11.2.7", "monaco-editor-core": "^0.55.1", @@ -183,7 +185,7 @@ "signal-exit": "^4.1.0", "sql.js": "^1.14.1", "systeminformation": "^5.30.6", - "twofish-ts": "^1.0.2", + "twofish": "^0.0.0", "unified": "^11.0.5", "unist-builder": "^4.0.0", "unist-util-remove": "^4.0.0", @@ -193,5 +195,5 @@ "yaml": "^2.8.2", "zod": "^4.3.6" }, - "workspaces": ["packages/site"] + "workspaces": ["packages/*"] } diff --git a/packages/twofish/Dockerfile b/packages/twofish/Dockerfile new file mode 100644 index 000000000..5d4992bce --- /dev/null +++ b/packages/twofish/Dockerfile @@ -0,0 +1,24 @@ +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + clang \ + lld \ + libclang-rt-dev \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Confirm wasm32 target is available +RUN clang --print-targets 2>&1 | grep -q wasm \ + || (echo "ERROR: clang has no WASM target" && exit 1) + +ARG UID=1000 +ARG GID=1000 + +RUN groupadd --gid ${GID} builder \ + && useradd --uid ${UID} --gid ${GID} --no-create-home builder + +USER builder + +WORKDIR /src + +CMD ["bash", "build.sh"] \ No newline at end of file diff --git a/packages/twofish/LICENSE b/packages/twofish/LICENSE new file mode 100644 index 000000000..6ca880784 --- /dev/null +++ b/packages/twofish/LICENSE @@ -0,0 +1,30 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: twofish +Upstream-Contact: Niels Ferguson +Source: http://www.macfergus.com/niels/code/TwofishClib-v0.3.zip + +Files: main.c twofish.c twofish.h +Copyright: 2002, Niels Ferguson. +License: other + + The author hereby grants a perpetual license to everybody to use this + code for any purpose as long as the copyright message is included in the + source code of this or any derived work. + . + Yes, this means that you, your company, your club, and anyone else can + use this code anywhere you want. You can change it and distribute it + under the GPL, include it in your commercial product without releasing + the source code, put it on the web, etc. The only thing you cannot do is + remove my copyright message, or distribute any source code based on this + implementation that does not include my copyright message. + . + I appreciate a mention in the documentation or credits, but I understand + if that is difficult to do. I also appreciate it if you tell me where + and why you used my code. + . + DISCLAIMER: As I'm giving away my work for free, I'm of course not going + to accept any liability of any form. This code, or the Twofish cipher, + might very well be flawed; you have been warned. This software is + provided as-is, without any kind of warrenty or guarantee. And that is + really all you can expect when you download code for free from the + Internet. diff --git a/packages/twofish/README.md b/packages/twofish/README.md new file mode 100644 index 000000000..192f89a89 --- /dev/null +++ b/packages/twofish/README.md @@ -0,0 +1,12 @@ +The twofish code is taken from https://sources.debian.org/src/twofish/0.3-5 + +## Build + +Requirements +- NodeJS +- Docker (for WASM compilation) +- Unix environment + +We build the WASM module in Docker, so if you use macOS, start a Docker service. + +Run `npm run build` to build WASM module and TypeScript files. diff --git a/packages/twofish/build.sh b/packages/twofish/build.sh new file mode 100755 index 000000000..e7725c2e0 --- /dev/null +++ b/packages/twofish/build.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# build.sh +set -euo pipefail + +CLANG=${CLANG:-clang} + +echo "→ Compiler: $($CLANG --version | head -1)" +echo "→ Building twofish.wasm ..." + +mkdir -p dist + +$CLANG \ + --target=wasm32-unknown-unknown \ + -fuse-ld=lld \ + -O3 \ + -nostdlib \ + -ffreestanding \ + -fno-builtin \ + -I. \ + -Isrc/c \ + -Wl,--no-entry \ + -Wl,--export=twofish_init \ + -Wl,--export=twofish_get_io_buffer \ + -Wl,--export=twofish_create_session \ + -Wl,--export=twofish_encrypt \ + -Wl,--export=twofish_decrypt \ + -Wl,--export=twofish_destroy_session \ + -o dist/twofish.wasm \ + $(find src -name '*.c') + +echo "✓ Done — $(wc -c < dist/twofish.wasm | tr -d ' ') bytes → dist/twofish.wasm" \ No newline at end of file diff --git a/packages/twofish/compose.yml b/packages/twofish/compose.yml new file mode 100644 index 000000000..d4c30f33a --- /dev/null +++ b/packages/twofish/compose.yml @@ -0,0 +1,13 @@ +services: + builder: + build: + context: . + dockerfile: Dockerfile + args: + UID: ${DOCKER_UID:-1000} + GID: ${DOCKER_GID:-1000} + volumes: + - .:/src + working_dir: /src + command: bash build.sh + user: "${DOCKER_UID:-1000}:${DOCKER_GID:-1000}" \ No newline at end of file diff --git a/packages/twofish/package.json b/packages/twofish/package.json new file mode 100644 index 000000000..cffeccf18 --- /dev/null +++ b/packages/twofish/package.json @@ -0,0 +1,30 @@ +{ + "name": "twofish", + "version": "0.0.0", + "description": "The Twofish encryption built via WASM", + "scripts": { + "build": "rm -rf dist && npm run build:wasm && npm run build:ts", + "build:wasm": "docker compose run --rm builder", + "build:ts": "tsdown --format esm --format cjs", + "dev": "tsdown --watch" + }, + "author": "Vitonsky ", + "license": "MIT", + "types": "./dist/index.d.cts", + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "files": ["dist"], + "devDependencies": { + "@types/node": "^25.5.0", + "tsdown": "^0.21.6", + "typescript": "^6.0.2" + }, + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json", + "./twofish.wasm": "./dist/twofish.wasm" + } +} diff --git a/packages/twofish/src/c/string.h b/packages/twofish/src/c/string.h new file mode 100644 index 000000000..a9d5630d7 --- /dev/null +++ b/packages/twofish/src/c/string.h @@ -0,0 +1,14 @@ +/* string.h + * Minimal stub for wasm32-unknown-unknown builds. + * The actual implementations live in twofish_wasm.c + */ +#ifndef _STRING_H +#define _STRING_H + +typedef unsigned long size_t; + +void *memset (void *dst, int c, size_t n); +void *memcpy (void *dst, const void *src, size_t n); +int memcmp (const void *a, const void *b, size_t n); + +#endif /* _STRING_H */ \ No newline at end of file diff --git a/packages/twofish/src/c/twofish.c b/packages/twofish/src/c/twofish.c new file mode 100644 index 000000000..75cc0aad3 --- /dev/null +++ b/packages/twofish/src/c/twofish.c @@ -0,0 +1,1690 @@ +/* + * Fast, portable, and easy-to-use Twofish implementation, + * Version 0.3. + * Copyright (c) 2002 by Niels Ferguson. + * (See further down for the almost-unrestricted licensing terms.) + * + * -------------------------------------------------------------------------- + * There are two files for this implementation: + * - twofish.h, the header file. + * - twofish.c, the code file. + * + * To incorporate this code into your program you should: + * - Check the licensing terms further down in this comment. + * - Fix the two type definitions in twofish.h to suit your platform. + * - Fix a few definitions in twofish.c in the section marked + * PLATFORM FIXES. There is one important ones that affects + * functionality, and then a few definitions that you can optimise + * for efficiency but those have no effect on the functionality. + * Don't change anything else. + * - Put the code in your project and compile it. + * + * To use this library you should: + * - Call Twofish_initialise() in your program before any other function in + * this library. + * - Use Twofish_prepare_key(...) to convert a key to internal form. + * - Use Twofish_encrypt(...) and Twofish_decrypt(...) to encrypt and decrypt + * data. + * See the comments in the header file for details on these functions. + * -------------------------------------------------------------------------- + * + * There are many Twofish implementation available for free on the web. + * Most of them are hard to integrate into your own program. + * As we like people to use our cipher, I thought I would make it easier. + * Here is a free and easy-to-integrate Twofish implementation in C. + * The latest version is always available from my personal home page at + * http://niels.ferguson.net/ + * + * Integrating library code into a project is difficult because the library + * header files interfere with the project's header files and code. + * And of course the project's header files interfere with the library code. + * I've tried to resolve these problems here. + * The header file of this implementation is very light-weight. + * It contains two typedefs, a structure, and a few function declarations. + * All names it defines start with "Twofish_". + * The header file is therefore unlikely to cause problems in your project. + * The code file of this implementation doesn't need to include the header + * files of the project. There is thus no danger of the project interfering + * with all the definitions and macros of the Twofish code. + * In most situations, all you need to do is fill in a few platform-specific + * definitions in the header file and code file, + * and you should be able to run the Twofish code in your project. + * I estimate it should take you less than an hour to integrate this code + * into your project, most of it spent reading the comments telling you what + * to do. + * + * For people using C++: it is very easy to wrap this library into a + * TwofishKey class. One of the big advantages is that you can automate the + * wiping of the key material in the destructor. I have not provided a C++ + * class because the interface depends too much on the abstract base class + * you use for block ciphers in your program, which I don't know about. + * + * This implementation is designed for use on PC-class machines. It uses the + * Twofish 'full' keying option which uses large tables. Total table size is + * around 5-6 kB for static tables plus 4.5 kB for each pre-processed key. + * If you need an implementation that uses less memory, + * take a look at Brian Gladman's code on his web site: + * http://fp.gladman.plus.com/cryptography_technology/aes/ + * He has code for all AES candidates. + * His Twofish code has lots of options trading off table size vs. speed. + * You can also take a look at the optimised code by Doug Whiting on the + * Twofish web site + * http://www.counterpane.com/twofish.html + * which has loads of options. + * I believe these existing implementations are harder to re-use because they + * are not clean libraries and they impose requirements on the environment. + * This implementation is very careful to minimise those, + * and should be easier to integrate into any larger program. + * + * The default mode of this implementation is fully portable as it uses no + * behaviour not defined in the C standard. (This is harder than you think.) + * If you have any problems porting the default mode, please let me know + * so that I can fix the problem. (But only if this code is at fault, I + * don't fix compilers.) + * Most of the platform fixes are related to non-portable but faster ways + * of implementing certain functions. + * + * In general I've tried to make the code as fast as possible, at the expense + * of memory and code size. However, C does impose limits, and this + * implementation will be slower than an optimised assembler implementation. + * But beware of assembler implementations: a good Pentium implementation + * uses completely different code than a good Pentium II implementation. + * You basically have to re-write the assembly code for every generation of + * processor. Unless you are severely pressed for speed, stick with C. + * + * The initialisation routine of this implementation contains a self-test. + * If initialisation succeeds without calling the fatal routine, then + * the implementation works. I don't think you can break the implementation + * in such a way that it still passes the tests, unless you are malicious. + * In other words: if the initialisation routine returns, + * you have successfully ported the implementation. + * (Or not implemented the fatal routine properly, but that is your problem.) + * + * I'm indebted to many people who helped me in one way or another to write + * this code. During the design of Twofish and the AES process I had very + * extensive discussions of all implementation issues with various people. + * Doug Whiting in particular provided a wealth of information. The Twofish + * team spent untold hours discussion various cipher features, and their + * implementation. Brian Gladman implemented all AES candidates in C, + * and we had some fruitful discussions on how to implement Twofish in C. + * Jan Nieuwenhuizen tested this code on Linux using GCC. + * + * Now for the license: + * The author hereby grants a perpetual license to everybody to + * use this code for any purpose as long as the copyright message is included + * in the source code of this or any derived work. + * + * Yes, this means that you, your company, your club, and anyone else + * can use this code anywhere you want. You can change it and distribute it + * under the GPL, include it in your commercial product without releasing + * the source code, put it on the web, etc. + * The only thing you cannot do is remove my copyright message, + * or distribute any source code based on this implementation that does not + * include my copyright message. + * + * I appreciate a mention in the documentation or credits, + * but I understand if that is difficult to do. + * I also appreciate it if you tell me where and why you used my code. + * + * Please send any questions or comments to niels@ferguson.net + * + * Have Fun! + * + * Niels + */ + +/* + * DISCLAIMER: As I'm giving away my work for free, I'm of course not going + * to accept any liability of any form. This code, or the Twofish cipher, + * might very well be flawed; you have been warned. + * This software is provided as-is, without any kind of warrenty or + * guarantee. And that is really all you can expect when you download + * code for free from the Internet. + * + * I think it is really sad that disclaimers like this seem to be necessary. + * If people only had a little bit more common sense, and didn't come + * whining like little children every time something happens.... + */ + +/* + * Version history: + * Version 0.0, 2002-08-30 + * First written. + * Version 0.1, 2002-09-03 + * Added disclaimer. Improved self-tests. + * Version 0.2, 2002-09-09 + * Removed last non-portabilities. Default now works completely within + * the C standard. UInt32 can be larger than 32 bits without problems. + * Version 0.3, 2002-09-28 + * Bugfix: use instead of to adhere to ANSI/ISO. + * Rename BIG_ENDIAN macro to CPU_IS_BIG_ENDIAN. The gcc library + * header already defines BIG_ENDIAN, even though it is not + * supposed to. + */ + + +/* + * Minimum set of include files. + * You should not need any application-specific include files for this code. + * In fact, adding you own header files could break one of the many macros or + * functions in this file. Be very careful. + * Standard include files will probably be ok. + */ +#include /* for memset(), memcpy(), and memcmp() */ +#include "twofish.h" + + +/* + * PLATFORM FIXES + * ============== + * + * Fix the type definitions in twofish.h first! + * + * The following definitions have to be fixed for each particular platform + * you work on. If you have a multi-platform program, you no doubt have + * portable definitions that you can substitute here without changing the + * rest of the code. + */ + + +/* + * Function called if something is fatally wrong with the implementation. + * This fatal function is called when a coding error is detected in the + * Twofish implementation, or when somebody passes an obviously erroneous + * parameter to this implementation. There is not much you can do when + * the code contains bugs, so we just stop. + * + * The argument is a string. Ideally the fatal function prints this string + * as an error message. Whatever else this function does, it should never + * return. A typical implementation would stop the program completely after + * printing the error message. + * + * This default implementation is not very useful, + * but does not assume anything about your environment. + * It will at least let you know something is wrong.... + * I didn't want to include any libraries to print and error or so, + * as this makes the code much harder to integrate in a project. + * + * Note that the Twofish_fatal function may not return to the caller. + * Unfortunately this is not something the self-test can test for, + * so you have to make sure of this yourself. + * + * If you want to call an external function, be careful about including + * your own header files here. This code uses a lot of macros, and your + * header file could easily break it. Maybe the best solution is to use + * a separate extern statement for your fatal function. + */ +__attribute__((noreturn)) extern void Twofish_fatal( const char *msg ); + + +/* + * The rest of the settings are not important for the functionality + * of this Twofish implementation. That is, their default settings + * work on all platforms. You can change them to improve the + * speed of the implementation on your platform. Erroneous settings + * will result in erroneous implementations, but the self-test should + * catch those. + */ + + +/* + * Macros to rotate a Twofish_UInt32 value left or right by the + * specified number of bits. This should be a 32-bit rotation, + * and not rotation of, say, 64-bit values. + * + * Every encryption or decryption operation uses 32 of these rotations, + * so it is a good idea to make these macros efficient. + * + * This fully portable definition has one piece of tricky stuff. + * The UInt32 might be larger than 32 bits, so we have to mask + * any higher bits off. The simplest way to do this is to 'and' the + * value first with 0xffffffff and then shift it right. An optimising + * compiler that has a 32-bit type can optimise this 'and' away. + * + * Unfortunately there is no portable way of writing the constant + * 0xffffffff. You don't know which suffix to use (U, or UL?) + * The UINT32_MASK definition uses a bit of trickery. Shift-left + * is only defined if the shift amount is strictly less than the size + * of the UInt32, so we can't use (1<<32). The answer it to take the value + * 2, cast it to a UInt32, shift it left 31 positions, and subtract one. + * Another example of how to make something very simple extremely difficult. + * I hate C. + * + * The rotation macros are straightforward. + * They are only applied to UInt32 values, which are _unsigned_ + * so the >> operator must do a logical shift that brings in zeroes. + * On most platforms you will only need to optimise the ROL32 macro; the + * ROR32 macro is not inefficient on an optimising compiler as all rotation + * amounts in this code are known at compile time. + * + * On many platforms there is a faster solution. + * For example, MS compilers have the __rotl and __rotr functions + * that generate x86 rotation instructions. + */ +#define UINT32_MASK ( (((UInt32)2)<<31) - 1 ) +#define ROL32( x, n ) ( (x)<<(n) | ((x) & UINT32_MASK) >> (32-(n)) ) +#define ROR32( x, n ) ROL32( (x), 32-(n) ) + + +/* + * Select data type for q-table entries. + * + * Larger entry types cost more memory (1.5 kB), and might be faster + * or slower depending on the CPU and compiler details. + * + * This choice only affects the static data size and the key setup speed. + * Functionality, expanded key size, or encryption speed are not affected. + * Define to 1 to get large q-table entries. + */ +#define LARGE_Q_TABLE 0 /* default = 0 */ + + +/* + * Method to select a single byte from a UInt32. + * WARNING: non-portable code if set; might not work on all platforms. + * + * Inside the inner loop of Twofish it is necessary to access the 4 + * individual bytes of a UInt32. This can be done using either shifts + * and masks, or memory accesses. + * + * Set to 0 to use shift and mask operations for the byte selection. + * This is more ALU intensive. It is also fully portable. + * + * Set to 1 to use memory accesses. The UInt32 is stored in memory and + * the individual bytes are read from memory one at a time. + * This solution is more memory-intensive, and not fully portable. + * It might be faster on your platform, or not. If you use this option, + * make sure you set the CPU_IS_BIG_ENDIAN flag appropriately. + * + * This macro does not affect the conversion of the inputs and outputs + * of the cipher. See the CONVERT_USING_CASTS macro for that. + */ +#define SELECT_BYTE_FROM_UINT32_IN_MEMORY 0 /* default = 0 */ + + +/* + * Method used to read the input and write the output. + * WARNING: non-portable code if set; might not work on all platforms. + * + * Twofish operates on 32-bit words. The input to the cipher is + * a byte array, as is the output. The portable method of doing the + * conversion is a bunch of rotate and mask operations, but on many + * platforms it can be done faster using a cast. + * This only works if your CPU allows UInt32 accesses to arbitrary Byte + * addresses. + * + * Set to 0 to use the shift and mask operations. This is fully + * portable. . + * + * Set to 1 to use a cast. The Byte * is cast to a UInt32 *, and a + * UInt32 is read. If necessary (as indicated by the CPU_IS_BIG_ENDIAN + * macro) the byte order in the UInt32 is swapped. The reverse is done + * to write the output of the encryption/decryption. Make sure you set + * the CPU_IS_BIG_ENDIAN flag appropriately. + * This option does not work unless a UInt32 is exactly 32 bits. + * + * This macro only changes the reading/writing of the plaintext/ciphertext. + * See the SELECT_BYTE_FROM_UINT32_IN_MEMORY to affect the way in which + * a UInt32 is split into 4 bytes for the S-box selection. + */ +#define CONVERT_USING_CASTS 0 /* default = 0 */ + + +/* + * Endianness switch. + * Only relevant if SELECT_BYTE_FROM_UINT32_IN_MEMORY or + * CONVERT_USING_CASTS is set. + * + * Set to 1 on a big-endian machine, and to 0 on a little-endian machine. + * Twofish uses the little-endian convention (least significant byte first) + * and big-endian machines (using most significant byte first) + * have to do a few conversions. + * + * CAUTION: This code has never been tested on a big-endian machine, + * because I don't have access to one. Feedback appreciated. + */ +#define CPU_IS_BIG_ENDIAN 0 + + +/* + * Macro to reverse the order of the bytes in a UInt32. + * Used to convert to little-endian on big-endian machines. + * This macro is always tested, but only used in the encryption and + * decryption if CONVERT_USING_CASTS, and CPU_IS_BIG_ENDIAN + * are both set. In other words: this macro is only speed-critical if + * both these flags have been set. + * + * This default definition of SWAP works, but on many platforms there is a + * more efficient implementation. + */ +#define BSWAP(x) ((ROL32((x),8) & 0x00ff00ff) | (ROR32((x),8) & 0xff00ff00)) + + +/* + * END OF PLATFORM FIXES + * ===================== + * + * You should not have to touch the rest of this file. + */ + + +/* + * Convert the external type names to some that are easier to use inside + * this file. I didn't want to use the names Byte and UInt32 in the + * header file, because many programs already define them and using two + * conventions at once can be very difficult. + * Don't change these definitions! Change the originals + * in twofish.h instead. + */ +/* A Byte must be an unsigned integer, 8 bits long. */ +typedef Twofish_Byte Byte; +/* A UInt32 must be an unsigned integer at least 32 bits long. */ +typedef Twofish_UInt32 UInt32; + + +/* + * Define a macro ENDIAN_CONVERT. + * + * We define a macro ENDIAN_CONVERT that performs a BSWAP on big-endian + * machines, and is the identity function on little-endian machines. + * The code then uses this macro without considering the endianness. + */ +#if CPU_IS_BIG_ENDIAN +#define ENDIAN_CONVERT(x) BSWAP(x) +#else +#define ENDIAN_CONVERT(x) (x) +#endif + + +/* + * Compute byte offset within a UInt32 stored in memory. + * + * This is only used when SELECT_BYTE_FROM_UINT32_IN_MEMORY is set. + * + * The input is the byte number 0..3, 0 for least significant. + * Note the use of sizeof() to support UInt32 types that are larger + * than 4 bytes. + */ +#if CPU_IS_BIG_ENDIAN +#define BYTE_OFFSET( n ) (sizeof(UInt32) - 1 - (n) ) +#else +#define BYTE_OFFSET( n ) (n) +#endif + + +/* + * Macro to get Byte no. b from UInt32 value X. + * We use two different definition, depending on the settings. + */ +#if SELECT_BYTE_FROM_UINT32_IN_MEMORY + /* Pick the byte from the memory in which X is stored. */ +#define SELECT_BYTE( X, b ) (((Byte *)(&(X)))[BYTE_OFFSET(b)]) +#else + /* Portable solution: Pick the byte directly from the X value. */ +#define SELECT_BYTE( X, b ) (((X) >> 8*(b)) & 0xff) +#endif + + +/* Some shorthands because we use byte selection in large formulae. */ +#define b0(X) SELECT_BYTE((X),0) +#define b1(X) SELECT_BYTE((X),1) +#define b2(X) SELECT_BYTE((X),2) +#define b3(X) SELECT_BYTE((X),3) + + +/* + * We need macros to load and store UInt32 from/to byte arrays + * using the least-significant-byte-first convention. + * + * GET32( p ) gets a UInt32 in lsb-first form from four bytes pointed to + * by p. + * PUT32( v, p ) writes the UInt32 value v at address p in lsb-first form. + */ +#if CONVERT_USING_CASTS + + /* Get UInt32 from four bytes pointed to by p. */ +#define GET32( p ) ENDIAN_CONVERT( *((UInt32 *)(p)) ) + /* Put UInt32 into four bytes pointed to by p */ +#define PUT32( v, p ) *((UInt32 *)(p)) = ENDIAN_CONVERT(v) + +#else + + /* Get UInt32 from four bytes pointed to by p. */ +#define GET32( p ) \ + ( \ + (UInt32)((p)[0]) \ + | (UInt32)((p)[1])<< 8\ + | (UInt32)((p)[2])<<16\ + | (UInt32)((p)[3])<<24\ + ) + /* Put UInt32 into four bytes pointed to by p */ +#define PUT32( v, p ) \ + (p)[0] = (Byte)(((v) ) & 0xff);\ + (p)[1] = (Byte)(((v) >> 8) & 0xff);\ + (p)[2] = (Byte)(((v) >> 16) & 0xff);\ + (p)[3] = (Byte)(((v) >> 24) & 0xff) + +#endif + + +/* + * Test the platform-specific macros. + * This function tests the macros defined so far to make sure the + * definitions are appropriate for this platform. + * If you make any mistake in the platform configuration, this should detect + * that and inform you what went wrong. + * Somewhere, someday, this is going to save somebody a lot of time, + * because misbehaving macros are hard to debug. + */ +static void test_platform() + { + /* Buffer with test values. */ + Byte buf[] = {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0}; + UInt32 C; + UInt32 x,y; + int i; + + /* + * Some sanity checks on the types that can't be done in compile time. + * A smart compiler will just optimise these tests away. + * The pre-processor doesn't understand different types, so we cannot + * do these checks in compile-time. + * + * I hate C. + * + * The first check in each case is to make sure the size is correct. + * The second check is to ensure that it is an unsigned type. + */ + if( ((UInt32) ((UInt32)1 << 31) == 0) || ((UInt32)-1 < 0) ) + { + Twofish_fatal( "Twofish code: Twofish_UInt32 type not suitable" ); + } + if( (sizeof( Byte ) != 1) || ((Byte)-1 < 0) ) + { + Twofish_fatal( "Twofish code: Twofish_Byte type not suitable" ); + } + + /* + * Sanity-check the endianness conversions. + * This is just an aid to find problems. If you do the endianness + * conversion macros wrong you will fail the full cipher test, + * but that does not help you find the error. + * Always make it easy to find the bugs! + * + * Detail: There is no fully portable way of writing UInt32 constants, + * as you don't know whether to use the U or UL suffix. Using only U you + * might only be allowed 16-bit constants. Using UL you might get 64-bit + * constants which cannot be stored in a UInt32 without warnings, and + * which generally behave subtly different from a true UInt32. + * As long as we're just comparing with the constant, + * we can always use the UL suffix and at worst lose some efficiency. + * I use a separate '32-bit constant' macro in most of my other code. + * + * I hate C. + * + * Start with testing GET32. We test it on all positions modulo 4 + * to make sure we can handly any position of inputs. (Some CPUs + * do not allow non-aligned accesses which we would do if you used + * the CONVERT_USING_CASTS option. + */ + if( GET32( buf ) != 0x78563412UL || GET32(buf+1) != 0x9a785634UL + || GET32( buf+2 ) != 0xbc9a7856UL || GET32(buf+3) != 0xdebc9a78UL ) + { + Twofish_fatal( "Twofish code: GET32 not implemented properly" ); + } + + /* + * We can now use GET32 to test PUT32. + * We don't test the shifted versions. If GET32 can do that then + * so should PUT32. + */ + C = GET32( buf ); + PUT32( 3*C, buf ); + if( GET32( buf ) != 0x69029c36UL ) + { + Twofish_fatal( "Twofish code: PUT32 not implemented properly" ); + } + + + /* Test ROL and ROR */ + for( i=1; i<32; i++ ) + { + /* Just a simple test. */ + x = ROR32( C, i ); + y = ROL32( C, i ); + x ^= (C>>i) ^ (C<<(32-i)); + y ^= (C<>(32-i)); + x |= y; + /* + * Now all we check is that x is zero in the least significant + * 32 bits. Using the UL suffix is safe here, as it doesn't matter + * if we get a larger type. + */ + if( (x & 0xffffffffUL) != 0 ) + { + Twofish_fatal( "Twofish ROL or ROR not properly defined." ); + } + } + + /* Test the BSWAP macro */ + if( (BSWAP(C)) != 0x12345678UL ) + { + /* + * The BSWAP macro should always work, even if you are not using it. + * A smart optimising compiler will just remove this entire test. + */ + Twofish_fatal( "BSWAP not properly defined." ); + } + + /* And we can test the b macros which use SELECT_BYTE. */ + if( (b0(C)!=0x12) || (b1(C) != 0x34) || (b2(C) != 0x56) || (b3(C) != 0x78) ) + { + /* + * There are many reasons why this could fail. + * Most likely is that CPU_IS_BIG_ENDIAN has the wrong value. + */ + Twofish_fatal( "Twofish code: SELECT_BYTE not implemented properly" ); + } + } + + +/* + * Finally, we can start on the Twofish-related code. + * You really need the Twofish specifications to understand this code. The + * best source is the Twofish book: + * "The Twofish Encryption Algorithm", by Bruce Schneier, John Kelsey, + * Doug Whiting, David Wagner, Chris Hall, and Niels Ferguson. + * you can also use the AES submission document of Twofish, which is + * available from my list of publications on my personal web site at + * http://niels.ferguson.net/. + * + * The first thing we do is write the testing routines. This is what the + * implementation has to satisfy in the end. We only test the external + * behaviour of the implementation of course. + */ + + +/* + * Perform a single self test on a (plaintext,ciphertext,key) triple. + * Arguments: + * key array of key bytes + * key_len length of key in bytes + * p plaintext + * c ciphertext + */ +static void test_vector( Byte key[], int key_len, Byte p[16], Byte c[16] ) + { + Byte tmp[16]; /* scratch pad. */ + Twofish_key xkey; /* The expanded key */ + int i; + + + /* Prepare the key */ + Twofish_prepare_key( key, key_len, &xkey ); + + /* + * We run the test twice to ensure that the xkey structure + * is not damaged by the first encryption. + * Those are hideous bugs to find if you get them in an application. + */ + for( i=0; i<2; i++ ) + { + /* Encrypt and test */ + Twofish_encrypt( &xkey, p, tmp ); + if( memcmp( c, tmp, 16 ) != 0 ) + { + Twofish_fatal( "Twofish encryption failure" ); + } + + /* Decrypt and test */ + Twofish_decrypt( &xkey, c, tmp ); + if( memcmp( p, tmp, 16 ) != 0 ) + { + Twofish_fatal( "Twofish decryption failure" ); + } + } + + /* The test keys are not secret, so we don't need to wipe xkey. */ + } + + +/* + * Check implementation using three (key,plaintext,ciphertext) + * test vectors, one for each major key length. + * + * This is an absolutely minimal self-test. + * This routine does not test odd-sized keys. + */ +static void test_vectors() + { + /* + * We run three tests, one for each major key length. + * These test vectors come from the Twofish specification. + * One encryption and one decryption using randomish data and key + * will detect almost any error, especially since we generate the + * tables ourselves, so we don't have the problem of a single + * damaged table entry in the source. + */ + + /* 128-bit test is the I=3 case of section B.2 of the Twofish book. */ + static Byte k128[] = { + 0x9F, 0x58, 0x9F, 0x5C, 0xF6, 0x12, 0x2C, 0x32, + 0xB6, 0xBF, 0xEC, 0x2F, 0x2A, 0xE8, 0xC3, 0x5A, + }; + static Byte p128[] = { + 0xD4, 0x91, 0xDB, 0x16, 0xE7, 0xB1, 0xC3, 0x9E, + 0x86, 0xCB, 0x08, 0x6B, 0x78, 0x9F, 0x54, 0x19 + }; + static Byte c128[] = { + 0x01, 0x9F, 0x98, 0x09, 0xDE, 0x17, 0x11, 0x85, + 0x8F, 0xAA, 0xC3, 0xA3, 0xBA, 0x20, 0xFB, 0xC3 + }; + + /* 192-bit test is the I=4 case of section B.2 of the Twofish book. */ + static Byte k192[] = { + 0x88, 0xB2, 0xB2, 0x70, 0x6B, 0x10, 0x5E, 0x36, + 0xB4, 0x46, 0xBB, 0x6D, 0x73, 0x1A, 0x1E, 0x88, + 0xEF, 0xA7, 0x1F, 0x78, 0x89, 0x65, 0xBD, 0x44 + }; + static Byte p192[] = { + 0x39, 0xDA, 0x69, 0xD6, 0xBA, 0x49, 0x97, 0xD5, + 0x85, 0xB6, 0xDC, 0x07, 0x3C, 0xA3, 0x41, 0xB2 + }; + static Byte c192[] = { + 0x18, 0x2B, 0x02, 0xD8, 0x14, 0x97, 0xEA, 0x45, + 0xF9, 0xDA, 0xAC, 0xDC, 0x29, 0x19, 0x3A, 0x65 + }; + + /* 256-bit test is the I=4 case of section B.2 of the Twofish book. */ + static Byte k256[] = { + 0xD4, 0x3B, 0xB7, 0x55, 0x6E, 0xA3, 0x2E, 0x46, + 0xF2, 0xA2, 0x82, 0xB7, 0xD4, 0x5B, 0x4E, 0x0D, + 0x57, 0xFF, 0x73, 0x9D, 0x4D, 0xC9, 0x2C, 0x1B, + 0xD7, 0xFC, 0x01, 0x70, 0x0C, 0xC8, 0x21, 0x6F + }; + static Byte p256[] = { + 0x90, 0xAF, 0xE9, 0x1B, 0xB2, 0x88, 0x54, 0x4F, + 0x2C, 0x32, 0xDC, 0x23, 0x9B, 0x26, 0x35, 0xE6 + }; + static Byte c256[] = { + 0x6C, 0xB4, 0x56, 0x1C, 0x40, 0xBF, 0x0A, 0x97, + 0x05, 0x93, 0x1C, 0xB6, 0xD4, 0x08, 0xE7, 0xFA + }; + + /* Run the actual tests. */ + test_vector( k128, 16, p128, c128 ); + test_vector( k192, 24, p192, c192 ); + test_vector( k256, 32, p256, c256 ); + } + + +/* + * Perform extensive test for a single key size. + * + * Test a single key size against the test vectors from section + * B.2 in the Twofish book. This is a sequence of 49 encryptions + * and decryptions. Each plaintext is equal to the ciphertext of + * the previous encryption. The key is made up from the ciphertext + * two and three encryptions ago. Both plaintext and key start + * at the zero value. + * We should have designed a cleaner recurrence relation for + * these tests, but it is too late for that now. At least we learned + * how to do it better next time. + * For details see appendix B of the book. + * + * Arguments: + * key_len Number of bytes of key + * final_value Final plaintext value after 49 iterations + */ +static void test_sequence( int key_len, Byte final_value[] ) + { + Byte buf[ (50+3)*16 ]; /* Buffer to hold our computation values. */ + Byte tmp[16]; /* Temp for testing the decryption. */ + Twofish_key xkey; /* The expanded key */ + int i; + Byte * p; + + /* Wipe the buffer */ + memset( buf, 0, sizeof( buf ) ); + + /* + * Because the recurrence relation is done in an inconvenient manner + * we end up looping backwards over the buffer. + */ + + /* Pointer in buffer points to current plaintext. */ + p = &buf[50*16]; + for( i=1; i<50; i++ ) + { + /* + * Prepare a key. + * This automatically checks that key_len is valid. + */ + Twofish_prepare_key( p+16, key_len, &xkey ); + + /* Compute the next 16 bytes in the buffer */ + Twofish_encrypt( &xkey, p, p-16 ); + + /* Check that the decryption is correct. */ + Twofish_decrypt( &xkey, p-16, tmp ); + if( memcmp( tmp, p, 16 ) != 0 ) + { + Twofish_fatal( "Twofish decryption failure in sequence" ); + } + /* Move on to next 16 bytes in the buffer. */ + p -= 16; + } + + /* And check the final value. */ + if( memcmp( p, final_value, 16 ) != 0 ) + { + Twofish_fatal( "Twofish encryption failure in sequence" ); + } + + /* None of the data was secret, so there is no need to wipe anything. */ + } + + +/* + * Run all three sequence tests from the Twofish test vectors. + * + * This checks the most extensive test vectors currently available + * for Twofish. The data is from the Twofish book, appendix B.2. + */ +static void test_sequences() + { + static Byte r128[] = { + 0x5D, 0x9D, 0x4E, 0xEF, 0xFA, 0x91, 0x51, 0x57, + 0x55, 0x24, 0xF1, 0x15, 0x81, 0x5A, 0x12, 0xE0 + }; + static Byte r192[] = { + 0xE7, 0x54, 0x49, 0x21, 0x2B, 0xEE, 0xF9, 0xF4, + 0xA3, 0x90, 0xBD, 0x86, 0x0A, 0x64, 0x09, 0x41 + }; + static Byte r256[] = { + 0x37, 0xFE, 0x26, 0xFF, 0x1C, 0xF6, 0x61, 0x75, + 0xF5, 0xDD, 0xF4, 0xC3, 0x3B, 0x97, 0xA2, 0x05 + }; + + /* Run the three sequence test vectors */ + test_sequence( 16, r128 ); + test_sequence( 24, r192 ); + test_sequence( 32, r256 ); + } + + +/* + * Test the odd-sized keys. + * + * Every odd-sized key is equivalent to a one of 128, 192, or 256 bits. + * The equivalent key is found by padding at the end with zero bytes + * until a regular key size is reached. + * + * We just test that the key expansion routine behaves properly. + * If the expanded keys are identical, then the encryptions and decryptions + * will behave the same. + */ +static void test_odd_sized_keys() + { + Byte buf[32]; + Twofish_key xkey; + Twofish_key xkey_two; + int i; + + /* + * We first create an all-zero key to use as PRNG key. + * Normally we would not have to fill the buffer with zeroes, as we could + * just pass a zero key length to the Twofish_prepare_key function. + * However, this relies on using odd-sized keys, and those are just the + * ones we are testing here. We can't use an untested function to test + * itself. + */ + memset( buf, 0, sizeof( buf ) ); + Twofish_prepare_key( buf, 16, &xkey ); + + /* Fill buffer with pseudo-random data derived from two encryptions */ + Twofish_encrypt( &xkey, buf, buf ); + Twofish_encrypt( &xkey, buf, buf+16 ); + + /* Create all possible shorter keys that are prefixes of the buffer. */ + for( i=31; i>=0; i-- ) + { + /* Set a byte to zero. This is the new padding byte */ + buf[i] = 0; + + /* Expand the key with only i bytes of length */ + Twofish_prepare_key( buf, i, &xkey ); + + /* Expand the corresponding padded key of regular length */ + Twofish_prepare_key( buf, i<=16 ? 16 : i<= 24 ? 24 : 32, &xkey_two ); + + /* Compare the two */ + if( memcmp( &xkey, &xkey_two, sizeof( xkey ) ) != 0 ) + { + Twofish_fatal( "Odd sized keys do not expand properly" ); + } + } + + /* None of the key values are secret, so we don't need to wipe them. */ + } + + +/* + * Test the Twofish implementation. + * + * This routine runs all the self tests, in order of importance. + * It is called by the Twofish_initialise routine. + * + * In almost all applications the cost of running the self tests during + * initialisation is insignificant, especially + * compared to the time it takes to load the application from disk. + * If you are very pressed for initialisation performance, + * you could remove some of the tests. Make sure you did run them + * once in the software and hardware configuration you are using. + */ +static void self_test() + { + /* The three test vectors form an absolute minimal test set. */ + test_vectors(); + + /* + * If at all possible you should run these tests too. They take + * more time, but provide a more thorough coverage. + */ + test_sequences(); + + /* Test the odd-sized keys. */ + test_odd_sized_keys(); + } + + +/* + * And now, the actual Twofish implementation. + * + * This implementation generates all the tables during initialisation. + * I don't like large tables in the code, especially since they are easily + * damaged in the source without anyone noticing it. You need code to + * generate them anyway, and this way all the code is close together. + * Generating them in the application leads to a smaller executable + * (the code is smaller than the tables it generates) and a + * larger static memory footprint. + * + * Twofish can be implemented in many ways. I have chosen to + * use large tables with a relatively long key setup time. + * If you encrypt more than a few blocks of data it pays to pre-compute + * as much as possible. This implementation is relatively inefficient for + * applications that need to re-key every block or so. + */ + +/* + * We start with the t-tables, directly from the Twofish definition. + * These are nibble-tables, but merging them and putting them two nibbles + * in one byte is more work than it is worth. + */ +static Byte t_table[2][4][16] = { + { + {0x8,0x1,0x7,0xD,0x6,0xF,0x3,0x2,0x0,0xB,0x5,0x9,0xE,0xC,0xA,0x4}, + {0xE,0xC,0xB,0x8,0x1,0x2,0x3,0x5,0xF,0x4,0xA,0x6,0x7,0x0,0x9,0xD}, + {0xB,0xA,0x5,0xE,0x6,0xD,0x9,0x0,0xC,0x8,0xF,0x3,0x2,0x4,0x7,0x1}, + {0xD,0x7,0xF,0x4,0x1,0x2,0x6,0xE,0x9,0xB,0x3,0x0,0x8,0x5,0xC,0xA} + }, + { + {0x2,0x8,0xB,0xD,0xF,0x7,0x6,0xE,0x3,0x1,0x9,0x4,0x0,0xA,0xC,0x5}, + {0x1,0xE,0x2,0xB,0x4,0xC,0x3,0x7,0x6,0xD,0xA,0x5,0xF,0x9,0x0,0x8}, + {0x4,0xC,0x7,0x5,0x1,0x6,0x9,0xA,0x0,0xE,0xD,0x8,0x2,0xB,0x3,0xF}, + {0xB,0x9,0x5,0x1,0xC,0x3,0xD,0xE,0x6,0x4,0x7,0xF,0x2,0x0,0x8,0xA} + } +}; + + +/* A 1-bit rotation of 4-bit values. Input must be in range 0..15 */ +#define ROR4BY1( x ) (((x)>>1) | (((x)<<3) & 0x8) ) + +/* + * The q-boxes are only used during the key schedule computations. + * These are 8->8 bit lookup tables. Some CPUs prefer to have 8->32 bit + * lookup tables as it is faster to load a 32-bit value than to load an + * 8-bit value and zero the rest of the register. + * The LARGE_Q_TABLE switch allows you to choose 32-bit entries in + * the q-tables. Here we just define the Qtype which is used to store + * the entries of the q-tables. + */ +#if LARGE_Q_TABLE +typedef UInt32 Qtype; +#else +typedef Byte Qtype; +#endif + +/* + * The actual q-box tables. + * There are two q-boxes, each having 256 entries. + */ +static Qtype q_table[2][256]; + + +/* + * Now the function that converts a single t-table into a q-table. + * + * Arguments: + * t[4][16] : four 4->4bit lookup tables that define the q-box + * q[256] : output parameter: the resulting q-box as a lookup table. + */ +static void make_q_table( Byte t[4][16], Qtype q[256] ) + { + int ae,be,ao,bo; /* Some temporaries. */ + int i; + /* Loop over all input values and compute the q-box result. */ + for( i=0; i<256; i++ ) { + /* + * This is straight from the Twofish specifications. + * + * The ae variable is used for the a_i values from the specs + * with even i, and ao for the odd i's. Similarly for the b's. + */ + ae = i>>4; be = i&0xf; + ao = ae ^ be; bo = ae ^ ROR4BY1(be) ^ ((ae<<3)&8); + ae = t[0][ao]; be = t[1][bo]; + ao = ae ^ be; bo = ae ^ ROR4BY1(be) ^ ((ae<<3)&8); + ae = t[2][ao]; be = t[3][bo]; + + /* Store the result in the q-box table, the cast avoids a warning. */ + q[i] = (Qtype) ((be<<4) | ae); + } + } + + +/* + * Initialise both q-box tables. + */ +static void initialise_q_boxes() { + /* Initialise each of the q-boxes using the t-tables */ + make_q_table( t_table[0], q_table[0] ); + make_q_table( t_table[1], q_table[1] ); + } + + +/* + * Next up is the MDS matrix multiplication. + * The MDS matrix multiplication operates in the field + * GF(2)[x]/p(x) with p(x)=x^8+x^6+x^5+x^3+1. + * If you don't understand this, read a book on finite fields. You cannot + * follow the finite-field computations without some background. + * + * In this field, multiplication by x is easy: shift left one bit + * and if bit 8 is set then xor the result with 0x169. + * + * The MDS coefficients use a multiplication by 1/x, + * or rather a division by x. This is easy too: first make the + * value 'even' (i.e. bit 0 is zero) by xorring with 0x169 if necessary, + * and then shift right one position. + * Even easier: shift right and xor with 0xb4 if the lsbit was set. + * + * The MDS coefficients are 1, EF, and 5B, and we use the fact that + * EF = 1 + 1/x + 1/x^2 + * 5B = 1 + 1/x^2 + * in this field. This makes multiplication by EF and 5B relatively easy. + * + * This property is no accident, the MDS matrix was designed to allow + * this implementation technique to be used. + * + * We have four MDS tables, each mapping 8 bits to 32 bits. + * Each table performs one column of the matrix multiplication. + * As the MDS is always preceded by q-boxes, each of these tables + * also implements the q-box just previous to that column. + */ + +/* The actual MDS tables. */ +static UInt32 MDS_table[4][256]; + +/* A small table to get easy conditional access to the 0xb4 constant. */ +static UInt32 mds_poly_divx_const[] = {0,0xb4}; + +/* Function to initialise the MDS tables. */ +static void initialise_mds_tables() + { + int i; + UInt32 q,qef,q5b; /* Temporary variables. */ + + /* Loop over all 8-bit input values */ + for( i=0; i<256; i++ ) + { + /* + * To save some work during the key expansion we include the last + * of the q-box layers from the h() function in these MDS tables. + */ + + /* We first do the inputs that are mapped through the q0 table. */ + q = q_table[0][i]; + /* + * Here we divide by x, note the table to get 0xb4 only if the + * lsbit is set. + * This sets qef = (1/x)*q in the finite field + */ + qef = (q >> 1) ^ mds_poly_divx_const[ q & 1 ]; + /* + * Divide by x again, and add q to get (1+1/x^2)*q. + * Note that (1+1/x^2) = 5B in the field, and addition in the field + * is exclusive or on the bits. + */ + q5b = (qef >> 1) ^ mds_poly_divx_const[ qef & 1 ] ^ q; + /* + * Add q5b to qef to set qef = (1+1/x+1/x^2)*q. + * Again, (1+1/x+1/x^2) = EF in the field. + */ + qef ^= q5b; + + /* + * Now that we have q5b = 5B * q and qef = EF * q + * we can fill two of the entries in the MDS matrix table. + * See the Twofish specifications for the order of the constants. + */ + MDS_table[1][i] = q <<24 | q5b<<16 | qef<<8 | qef; + MDS_table[3][i] = q5b<<24 | qef<<16 | q <<8 | q5b; + + /* Now we do it all again for the two columns that have a q1 box. */ + q = q_table[1][i]; + qef = (q >> 1) ^ mds_poly_divx_const[ q & 1 ]; + q5b = (qef >> 1) ^ mds_poly_divx_const[ qef & 1 ] ^ q; + qef ^= q5b; + + /* The other two columns use the coefficient in a different order. */ + MDS_table[0][i] = qef<<24 | qef<<16 | q5b<<8 | q ; + MDS_table[2][i] = qef<<24 | q <<16 | qef<<8 | q5b; + } + } + + +/* + * The h() function is the heart of the Twofish cipher. + * It is a complicated sequence of q-box lookups, key material xors, + * and finally the MDS matrix. + * We use lots of macros to make this reasonably fast. + */ + +/* First a shorthand for the two q-tables */ +#define q0 q_table[0] +#define q1 q_table[1] + +/* + * Each macro computes one column of the h for either 2, 3, or 4 stages. + * As there are 4 columns, we have 12 macros in all. + * + * The key bytes are stored in the Byte array L at offset + * 0,1,2,3, 8,9,10,11, [16,17,18,19, [24,25,26,27]] as this is the + * order we get the bytes from the user. If you look at the Twofish + * specs, you'll see that h() is applied to the even key words or the + * odd key words. The bytes of the even words appear in this spacing, + * and those of the odd key words too. + * + * These macros are the only place where the q-boxes and the MDS table + * are used. + */ +#define H02( y, L ) MDS_table[0][q0[q0[y]^L[ 8]]^L[0]] +#define H12( y, L ) MDS_table[1][q0[q1[y]^L[ 9]]^L[1]] +#define H22( y, L ) MDS_table[2][q1[q0[y]^L[10]]^L[2]] +#define H32( y, L ) MDS_table[3][q1[q1[y]^L[11]]^L[3]] +#define H03( y, L ) H02( q1[y]^L[16], L ) +#define H13( y, L ) H12( q1[y]^L[17], L ) +#define H23( y, L ) H22( q0[y]^L[18], L ) +#define H33( y, L ) H32( q0[y]^L[19], L ) +#define H04( y, L ) H03( q1[y]^L[24], L ) +#define H14( y, L ) H13( q0[y]^L[25], L ) +#define H24( y, L ) H23( q0[y]^L[26], L ) +#define H34( y, L ) H33( q1[y]^L[27], L ) + +/* + * Now we can define the h() function given an array of key bytes. + * This function is only used in the key schedule, and not to pre-compute + * the keyed S-boxes. + * + * In the key schedule, the input is always of the form k*(1+2^8+2^16+2^24) + * so we only provide k as an argument. + * + * Arguments: + * k input to the h() function. + * L pointer to array of key bytes at + * offsets 0,1,2,3, ... 8,9,10,11, [16,17,18,19, [24,25,26,27]] + * kCycles # key cycles, 2, 3, or 4. + */ +static UInt32 h( int k, Byte L[], int kCycles ) + { + switch( kCycles ) { + /* We code all 3 cases separately for speed reasons. */ + case 2: + return H02(k,L) ^ H12(k,L) ^ H22(k,L) ^ H32(k,L); + case 3: + return H03(k,L) ^ H13(k,L) ^ H23(k,L) ^ H33(k,L); + case 4: + return H04(k,L) ^ H14(k,L) ^ H24(k,L) ^ H34(k,L); + default: + /* This is always a coding error, which is fatal. */ + Twofish_fatal( "Twofish h(): Illegal argument" ); + } + } + + +/* + * Pre-compute the keyed S-boxes. + * Fill the pre-computed S-box array in the expanded key structure. + * Each pre-computed S-box maps 8 bits to 32 bits. + * + * The S argument contains half the number of bytes of the full key, but is + * derived from the full key. (See Twofish specifications for details.) + * S has the weird byte input order used by the Hxx macros. + * + * This function takes most of the time of a key expansion. + * + * Arguments: + * S pointer to array of 8*kCycles Bytes containing the S vector. + * kCycles number of key words, must be in the set {2,3,4} + * xkey pointer to Twofish_key structure that will contain the S-boxes. + */ +static void fill_keyed_sboxes( Byte S[], int kCycles, Twofish_key * xkey ) + { + int i; + switch( kCycles ) { + /* We code all 3 cases separately for speed reasons. */ + case 2: + for( i=0; i<256; i++ ) + { + xkey->s[0][i]= H02( i, S ); + xkey->s[1][i]= H12( i, S ); + xkey->s[2][i]= H22( i, S ); + xkey->s[3][i]= H32( i, S ); + } + break; + case 3: + for( i=0; i<256; i++ ) + { + xkey->s[0][i]= H03( i, S ); + xkey->s[1][i]= H13( i, S ); + xkey->s[2][i]= H23( i, S ); + xkey->s[3][i]= H33( i, S ); + } + break; + case 4: + for( i=0; i<256; i++ ) + { + xkey->s[0][i]= H04( i, S ); + xkey->s[1][i]= H14( i, S ); + xkey->s[2][i]= H24( i, S ); + xkey->s[3][i]= H34( i, S ); + } + break; + default: + /* This is always a coding error, which is fatal. */ + Twofish_fatal( "Twofish fill_keyed_sboxes(): Illegal argument" ); + } + } + + +/* A flag to keep track of whether we have been initialised or not. */ +static int Twofish_initialised = 0; + +/* + * Initialise the Twofish implementation. + * This function must be called before any other function in the + * Twofish implementation is called. + * This routine also does some sanity checks, to make sure that + * all the macros behave, and it tests the whole cipher. + */ +void Twofish_initialise() + { + /* First test the various platform-specific definitions. */ + test_platform(); + + /* We can now generate our tables, in the right order of course. */ + initialise_q_boxes(); + initialise_mds_tables(); + + /* We're finished with the initialisation itself. */ + Twofish_initialised = 1; + + /* + * And run some tests on the whole cipher. + * Yes, you need to do this every time you start your program. + * It is called assurance; you have to be certain that your program + * still works properly. + */ + self_test(); + } + + +/* + * The Twofish key schedule uses an Reed-Solomon code matrix multiply. + * Just like the MDS matrix, the RS-matrix is designed to be easy + * to implement. Details are below in the code. + * + * These constants make it easy to compute in the finite field used + * for the RS code. + * + * We use Bytes for the RS computation, but these are automatically + * widened to unsigned integers in the expressions. Having unsigned + * ints in these tables therefore provides the fastest access. + */ +static unsigned int rs_poly_const[] = {0, 0x14d}; +static unsigned int rs_poly_div_const[] = {0, 0xa6 }; + + +/* + * Prepare a key for use in encryption and decryption. + * Like most block ciphers, Twofish allows the key schedule + * to be pre-computed given only the key. + * Twofish has a fairly 'heavy' key schedule that takes a lot of time + * to compute. The main work is pre-computing the S-boxes used in the + * encryption and decryption. We feel that this makes the cipher much + * harder to attack. The attacker doesn't even know what the S-boxes + * contain without including the entire key schedule in the analysis. + * + * Unlike most Twofish implementations, this one allows any key size from + * 0 to 32 bytes. Odd key sizes are defined for Twofish (see the + * specifications); the key is simply padded with zeroes to the next real + * key size of 16, 24, or 32 bytes. + * Each odd-sized key is thus equivalent to a single normal-sized key. + * + * Arguments: + * key array of key bytes + * key_len number of bytes in the key, must be in the range 0,...,32. + * xkey Pointer to an Twofish_key structure that will be filled + * with the internal form of the cipher key. + */ +void Twofish_prepare_key( Byte key[], int key_len, Twofish_key * xkey ) + { + /* We use a single array to store all key material in, + * to simplify the wiping of the key material at the end. + * The first 32 bytes contain the actual (padded) cipher key. + * The next 32 bytes contain the S-vector in its weird format, + * and we have 4 bytes of overrun necessary for the RS-reduction. + */ + Byte K[32+32+4]; + + int kCycles; /* # key cycles, 2,3, or 4. */ + + int i; + UInt32 A, B; /* Used to compute the round keys. */ + + Byte * kptr; /* Three pointers for the RS computation. */ + Byte * sptr; + Byte * t; + + Byte b,bx,bxx; /* Some more temporaries for the RS computation. */ + + /* Check that the Twofish implementation was initialised. */ + if( Twofish_initialised == 0 ) + { + /* + * You didn't call Twofish_initialise before calling this routine. + * This is a programming error, and therefore we call the fatal + * routine. + * + * I could of course call the initialisation routine here, + * but there are a few reasons why I don't. First of all, the + * self-tests have to be done at startup. It is no good to inform + * the user that the cipher implementation fails when he wants to + * write his data to disk in encrypted form. You have to warn him + * before he spends time typing his data. Second, the initialisation + * and self test are much slower than a single key expansion. + * Calling the initialisation here makes the performance of the + * cipher unpredictable. This can lead to really weird problems + * if you use the cipher for a real-time task. Suddenly it fails + * once in a while the first time you try to use it. Things like + * that are almost impossible to debug. + */ + Twofish_fatal( "Twofish implementation was not initialised." ); + + /* + * There is always a danger that the Twofish_fatal routine returns, + * in spite of the specifications that it should not. + * (A good programming rule: don't trust the rest of the code.) + * This would be disasterous. If the q-tables and MDS-tables have + * not been initialised, they are probably still filled with zeroes. + * Suppose the MDS-tables are all zero. The key expansion would then + * generate all-zero round keys, and all-zero s-boxes. The danger + * is that nobody would notice as the encryption function still + * mangles the input, and the decryption still 'decrypts' it, + * but now in a completely key-independent manner. + * To stop such security disasters, we use blunt force. + * If your program hangs here: fix the fatal routine! + */ + for(;;); /* Infinite loop, which beats being insecure. */ + } + + /* Check for valid key length. */ + if( key_len < 0 || key_len > 32 ) + { + /* + * This can only happen if a programmer didn't read the limitations + * on the key size. + */ + Twofish_fatal( "Twofish_prepare_key: illegal key length" ); + /* + * A return statement just in case the fatal macro returns. + * The rest of the code assumes that key_len is in range, and would + * buffer-overflow if it wasn't. + * + * Why do we still use a programming language that has problems like + * buffer overflows, when these problems were solved in 1960 with + * the development of Algol? Have we not leared anything? + */ + return; + } + + /* Pad the key with zeroes to the next suitable key length. */ + memcpy( K, key, key_len ); + memset( K+key_len, 0, sizeof(K)-key_len ); + + /* + * Compute kCycles: the number of key cycles used in the cipher. + * 2 for 128-bit keys, 3 for 192-bit keys, and 4 for 256-bit keys. + */ + kCycles = (key_len + 7) >> 3; + /* Handle the special case of very short keys: minimum 2 cycles. */ + if( kCycles < 2 ) + { + kCycles = 2; + } + + /* + * From now on we just pretend to have 8*kCycles bytes of + * key material in K. This handles all the key size cases. + */ + + /* + * We first compute the 40 expanded key words, + * formulas straight from the Twofish specifications. + */ + for( i=0; i<40; i+=2 ) + { + /* + * Due to the byte spacing expected by the h() function + * we can pick the bytes directly from the key K. + * As we use bytes, we never have the little/big endian + * problem. + * + * Note that we apply the rotation function only to simple + * variables, as the rotation macro might evaluate its argument + * more than once. + */ + A = h( i , K , kCycles ); + B = h( i+1, K+4, kCycles ); + B = ROL32( B, 8 ); + + /* Compute and store the round keys. */ + A += B; + B += A; + xkey->K[i] = A; + xkey->K[i+1] = ROL32( B, 9 ); + } + + /* Wipe variables that contained key material. */ + A=B=0; + + /* + * And now the dreaded RS multiplication that few seem to understand. + * The RS matrix is not random, and is specially designed to compute the + * RS matrix multiplication in a simple way. + * + * We work in the field GF(2)[x]/x^8+x^6+x^3+x^2+1. Note that this is a + * different field than used for the MDS matrix. + * (At least, it is a different representation because all GF(2^8) + * representations are equivalent in some form.) + * + * We take 8 consecutive bytes of the key and interpret them as + * a polynomial k_0 + k_1 y + k_2 y^2 + ... + k_7 y^7 where + * the k_i bytes are the key bytes and are elements of the finite field. + * We multiply this polynomial by y^4 and reduce it modulo + * y^4 + (x + 1/x)y^3 + (x)y^2 + (x + 1/x)y + 1. + * using straightforward polynomial modulo reduction. + * The coefficients of the result are the result of the RS + * matrix multiplication. When we wrote the Twofish specification, + * the original RS definition used the polynomials, + * but that requires much more mathematical knowledge. + * We were already using matrix multiplication in a finite field for + * the MDS matrix, so I re-wrote the RS operation as a matrix + * multiplication to reduce the difficulty of understanding it. + * Some implementors have not picked up on this simpler method of + * computing the RS operation, even though it is mentioned in the + * specifications. + * + * It is possible to perform these computations faster by using 32-bit + * word operations, but that is not portable and this is not a speed- + * critical area. + * + * We explained the 1/x computation when we did the MDS matrix. + * + * The S vector is stored in K[32..64]. + * The S vector has to be reversed, so we loop cross-wise. + * + * Note the weird byte spacing of the S-vector, to match the even + * or odd key words arrays. See the discussion at the Hxx macros for + * details. + */ + kptr = K + 8*kCycles; /* Start at end of key */ + sptr = K + 32; /* Start at start of S */ + + /* Loop over all key material */ + while( kptr > K ) + { + kptr -= 8; + /* + * Initialise the polynimial in sptr[0..12] + * The first four coefficients are 0 as we have to multiply by y^4. + * The next 8 coefficients are from the key material. + */ + memset( sptr, 0, 4 ); + memcpy( sptr+4, kptr, 8 ); + + /* + * The 12 bytes starting at sptr are now the coefficients of + * the polynomial we need to reduce. + */ + + /* Loop over the polynomial coefficients from high to low */ + t = sptr+11; + /* Keep looping until polynomial is degree 3; */ + while( t > sptr+3 ) + { + /* Pick up the highest coefficient of the poly. */ + b = *t; + + /* + * Compute x and (x+1/x) times this coefficient. + * See the MDS matrix implementation for a discussion of + * multiplication by x and 1/x. We just use different + * constants here as we are in a + * different finite field representation. + * + * These two statements set + * bx = (x) * b + * bxx= (x + 1/x) * b + */ + bx = (Byte)((b<<1) ^ rs_poly_const[ b>>7 ]); + bxx= (Byte)((b>>1) ^ rs_poly_div_const[ b&1 ] ^ bx); + + /* + * Subtract suitable multiple of + * y^4 + (x + 1/x)y^3 + (x)y^2 + (x + 1/x)y + 1 + * from the polynomial, except that we don't bother + * updating t[0] as it will become zero anyway. + */ + t[-1] ^= bxx; + t[-2] ^= bx; + t[-3] ^= bxx; + t[-4] ^= b; + + /* Go to the next coefficient. */ + t--; + } + + /* Go to next S-vector word, obeying the weird spacing rules. */ + sptr += 8; + } + + /* Wipe variables that contained key material. */ + b = bx = bxx = 0; + + /* And finally, we can compute the key-dependent S-boxes. */ + fill_keyed_sboxes( &K[32], kCycles, xkey ); + + /* Wipe array that contained key material. */ + memset( K, 0, sizeof( K ) ); + } + + +/* + * We can now start on the actual encryption and decryption code. + * As these are often speed-critical we will use a lot of macros. + */ + +/* + * The g() function is the heart of the round function. + * We have two versions of the g() function, one without an input + * rotation and one with. + * The pre-computed S-boxes make this pretty simple. + */ +#define g0(X,xkey) \ + (xkey->s[0][b0(X)]^xkey->s[1][b1(X)]^xkey->s[2][b2(X)]^xkey->s[3][b3(X)]) + +#define g1(X,xkey) \ + (xkey->s[0][b3(X)]^xkey->s[1][b0(X)]^xkey->s[2][b1(X)]^xkey->s[3][b2(X)]) + +/* + * A single round of Twofish. The A,B,C,D are the four state variables, + * T0 and T1 are temporaries, xkey is the expanded key, and r the + * round number. + * + * Note that this macro does not implement the swap at the end of the round. + */ +#define ENCRYPT_RND( A,B,C,D, T0, T1, xkey, r ) \ + T0 = g0(A,xkey); T1 = g1(B,xkey);\ + C ^= T0+T1+xkey->K[8+2*(r)]; C = ROR32(C,1);\ + D = ROL32(D,1); D ^= T0+2*T1+xkey->K[8+2*(r)+1] + +/* + * Encrypt a single cycle, consisting of two rounds. + * This avoids the swapping of the two halves. + * Parameter r is now the cycle number. + */ +#define ENCRYPT_CYCLE( A, B, C, D, T0, T1, xkey, r ) \ + ENCRYPT_RND( A,B,C,D,T0,T1,xkey,2*(r) );\ + ENCRYPT_RND( C,D,A,B,T0,T1,xkey,2*(r)+1 ) + +/* Full 16-round encryption */ +#define ENCRYPT( A,B,C,D,T0,T1,xkey ) \ + ENCRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 0 );\ + ENCRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 1 );\ + ENCRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 2 );\ + ENCRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 3 );\ + ENCRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 4 );\ + ENCRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 5 );\ + ENCRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 6 );\ + ENCRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 7 ) + +/* + * A single round of Twofish for decryption. It differs from + * ENCRYTP_RND only because of the 1-bit rotations. + */ +#define DECRYPT_RND( A,B,C,D, T0, T1, xkey, r ) \ + T0 = g0(A,xkey); T1 = g1(B,xkey);\ + C = ROL32(C,1); C ^= T0+T1+xkey->K[8+2*(r)];\ + D ^= T0+2*T1+xkey->K[8+2*(r)+1]; D = ROR32(D,1) + +/* + * Decrypt a single cycle, consisting of two rounds. + * This avoids the swapping of the two halves. + * Parameter r is now the cycle number. + */ +#define DECRYPT_CYCLE( A, B, C, D, T0, T1, xkey, r ) \ + DECRYPT_RND( A,B,C,D,T0,T1,xkey,2*(r)+1 );\ + DECRYPT_RND( C,D,A,B,T0,T1,xkey,2*(r) ) + +/* Full 16-round decryption. */ +#define DECRYPT( A,B,C,D,T0,T1, xkey ) \ + DECRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 7 );\ + DECRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 6 );\ + DECRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 5 );\ + DECRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 4 );\ + DECRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 3 );\ + DECRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 2 );\ + DECRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 1 );\ + DECRYPT_CYCLE( A,B,C,D,T0,T1,xkey, 0 ) + +/* + * A macro to read the state from the plaintext and do the initial key xors. + * The koff argument allows us to use the same macro + * for the decryption which uses different key words at the start. + */ +#define GET_INPUT( src, A,B,C,D, xkey, koff ) \ + A = GET32(src )^xkey->K[ koff]; B = GET32(src+ 4)^xkey->K[1+koff]; \ + C = GET32(src+ 8)^xkey->K[2+koff]; D = GET32(src+12)^xkey->K[3+koff] + +/* + * Similar macro to put the ciphertext in the output buffer. + * We xor the keys into the state variables before we use the PUT32 + * macro as the macro might use its argument multiple times. + */ +#define PUT_OUTPUT( A,B,C,D, dst, xkey, koff ) \ + A ^= xkey->K[ koff]; B ^= xkey->K[1+koff]; \ + C ^= xkey->K[2+koff]; D ^= xkey->K[3+koff]; \ + PUT32( A, dst ); PUT32( B, dst+ 4 ); \ + PUT32( C, dst+8 ); PUT32( D, dst+12 ) + + +/* + * Twofish block encryption + * + * Arguments: + * xkey expanded key array + * p 16 bytes of plaintext + * c 16 bytes in which to store the ciphertext + */ +void Twofish_encrypt( Twofish_key * xkey, Byte p[16], Byte c[16]) + { + UInt32 A,B,C,D,T0,T1; /* Working variables */ + + /* Get the four plaintext words xorred with the key */ + GET_INPUT( p, A,B,C,D, xkey, 0 ); + + /* Do 8 cycles (= 16 rounds) */ + ENCRYPT( A,B,C,D,T0,T1,xkey ); + + /* Store them with the final swap and the output whitening. */ + PUT_OUTPUT( C,D,A,B, c, xkey, 4 ); + } + + +/* + * Twofish block decryption. + * + * Arguments: + * xkey expanded key array + * p 16 bytes of plaintext + * c 16 bytes in which to store the ciphertext + */ +void Twofish_decrypt( Twofish_key * xkey, Byte c[16], Byte p[16]) + { + UInt32 A,B,C,D,T0,T1; /* Working variables */ + + /* Get the four plaintext words xorred with the key */ + GET_INPUT( c, A,B,C,D, xkey, 4 ); + + /* Do 8 cycles (= 16 rounds) */ + DECRYPT( A,B,C,D,T0,T1,xkey ); + + /* Store them with the final swap and the output whitening. */ + PUT_OUTPUT( C,D,A,B, p, xkey, 0 ); + } + +/* + * Using the macros it is easy to make special routines for + * CBC mode, CTR mode etc. The only thing you might want to + * add is a XOR_PUT_OUTPUT which xors the outputs into the + * destinationa instead of overwriting the data. This requires + * a XOR_PUT32 macro as well, but that should all be trivial. + * + * I thought about including routines for the separate cipher + * modes here, but it is unclear which modes should be included, + * and each encryption or decryption routine takes up a lot of code space. + * Also, I don't have any test vectors for any cipher modes + * with Twofish. + */ diff --git a/packages/twofish/src/c/twofish.h b/packages/twofish/src/c/twofish.h new file mode 100644 index 000000000..9e2e86b63 --- /dev/null +++ b/packages/twofish/src/c/twofish.h @@ -0,0 +1,191 @@ +/* + * Fast, portable, and easy-to-use Twofish implementation, + * Version 0.3. + * Copyright (c) 2002 by Niels Ferguson. + * + * See the twofish.c file for the details of the how and why of this code. + * + * The author hereby grants a perpetual license to everybody to + * use this code for any purpose as long as the copyright message is included + * in the source code of this or any derived work. + */ + +#ifndef _TWOFISH_H +# define _TWOFISH_H + +# ifdef __cplusplus +extern "C" { +# endif + +/* + * PLATFORM FIXES + * ============== + * + * The following definitions have to be fixed for each particular platform + * you work on. If you have a multi-platform program, you no doubt have + * portable definitions that you can substitute here without changing + * the rest of the code. + * + * The defaults provided here should work on most PC compilers. + */ + + +/* + * A Twofish_Byte must be an unsigned 8-bit integer. + * It must also be the elementary data size of your C platform, + * i.e. sizeof( Twofish_Byte ) == 1. + */ +typedef unsigned char Twofish_Byte; + +/* + * A Twofish_UInt32 must be an unsigned integer of at least 32 bits. + * + * This type is used only internally in the implementation, so ideally it + * would not appear in the header file, but it is used inside the + * Twofish_key structure which means it has to be included here. + */ +typedef unsigned int Twofish_UInt32; + + +/* + * END OF PLATFORM FIXES + * ===================== + * + * You should not have to touch the rest of this file, but the code + * in twofish.c has a few things you need to fix too. + */ + + +/* + * Structure that contains a prepared Twofish key. + * A cipher key is used in two stages. In the first stage it is converted + * form the original form to an internal representation. + * This internal form is then used to encrypt and decrypt data. + * This structure contains the internal form. It is rather large: 4256 bytes + * on a platform with 32-bit unsigned values. + * + * Treat this as an opague structure, and don't try to manipulate the + * elements in it. I wish I could hide the inside of the structure, + * but C doesn't allow that. + */ +typedef + struct + { + Twofish_UInt32 s[4][256]; /* pre-computed S-boxes */ + Twofish_UInt32 K[40]; /* Round key words */ + } + Twofish_key; + + +/* + * Initialise and test the Twofish implementation. + * + * This function MUST be called before any other function in the + * Twofish implementation is called. + * It only needs to be called once. + * + * Apart from initialising the implementation it performs a self test. + * If the Twofish_fatal function is not called, the code passed the test. + * (See the twofish.c file for details on the Twofish_fatal function.) + */ +extern void Twofish_initialise(); + + +/* + * Convert a cipher key to the internal form used for + * encryption and decryption. + * + * The cipher key is an array of bytes; the Twofish_Byte type is + * defined above to a type suitable on your platform. + * + * Any key must be converted to an internal form in the Twofisk_key structure + * before it can be used. + * The encryption and decryption functions only work with the internal form. + * The conversion to internal form need only be done once for each key value. + * + * Be sure to wipe all key storage, including the Twofish_key structure, + * once you are done with the key data. + * A simple memset( TwofishKey, 0, sizeof( TwofishKey ) ) will do just fine. + * + * Unlike most implementations, this one allows any key size from 0 bytes + * to 32 bytes. According to the Twofish specifications, + * irregular key sizes are handled by padding the key with zeroes at the end + * until the key size is 16, 24, or 32 bytes, whichever + * comes first. Note that each key of irregular size is equivalent to exactly + * one key of 16, 24, or 32 bytes. + * + * WARNING: Short keys have low entropy, and result in low security. + * Anything less than 8 bytes is utterly insecure. For good security + * use at least 16 bytes. I prefer to use 32-byte keys to prevent + * any collision attacks on the key. + * + * The key length argument key_len must be in the proper range. + * If key_len is not in the range 0,...,32 this routine attempts to generate + * a fatal error (depending on the code environment), + * and at best (or worst) returns without having done anything. + * + * Arguments: + * key Array of key bytes + * key_len Number of key bytes, must be in the range 0,1,...,32. + * xkey Pointer to an Twofish_key structure that will be filled + * with the internal form of the cipher key. + */ +extern void Twofish_prepare_key( + Twofish_Byte key[], + int key_len, + Twofish_key * xkey + ); + + +/* + * Encrypt a single block of data. + * + * This function encrypts a single block of 16 bytes of data. + * If you want to encrypt a larger or variable-length message, + * you will have to use a cipher mode, such as CBC or CTR. + * These are outside the scope of this implementation. + * + * The xkey structure is not modified by this routine, and can be + * used for further encryption and decryption operations. + * + * Arguments: + * xkey pointer to Twofish_key, internal form of the key + * produces by Twofish_prepare_key() + * p Plaintext to be encrypted + * c Place to store the ciphertext + */ +extern void Twofish_encrypt( + Twofish_key * xkey, + Twofish_Byte p[16], + Twofish_Byte c[16] + ); + + +/* + * Decrypt a single block of data. + * + * This function decrypts a single block of 16 bytes of data. + * If you want to decrypt a larger or variable-length message, + * you will have to use a cipher mode, such as CBC or CTR. + * These are outside the scope of this implementation. + * + * The xkey structure is not modified by this routine, and can be + * used for further encryption and decryption operations. + * + * Arguments: + * xkey pointer to Twofish_key, internal form of the key + * produces by Twofish_prepare_key() + * c Ciphertext to be decrypted + * p Place to store the plaintext + */ +extern void Twofish_decrypt( + Twofish_key * xkey, + Twofish_Byte c[16], + Twofish_Byte p[16] + ); + +# ifdef __cplusplus +} +# endif + +#endif /* !_TWOFISH_H */ diff --git a/packages/twofish/src/c/twofish_wasm.c b/packages/twofish/src/c/twofish_wasm.c new file mode 100644 index 000000000..f72e60297 --- /dev/null +++ b/packages/twofish/src/c/twofish_wasm.c @@ -0,0 +1,258 @@ +/* + * twofish_wasm.c + * + * Thin WASM wrapper around the Twofish reference implementation. + * Provides a session-based API with integer handles for JS consumption. + * + * Compile with: + * clang --target=wasm32-unknown-unknown -O3 -nostdlib \ + * -Wl,--no-entry -Wl,--allow-undefined \ + * -Wl,--export=twofish_init \ + * -Wl,--export=twofish_create_session \ + * -Wl,--export=twofish_encrypt \ + * -Wl,--export=twofish_decrypt \ + * -Wl,--export=twofish_destroy_session \ + * -Wl,--export=twofish_get_io_buffer \ + * -Wl,--export=__heap_base \ + * -o twofish.wasm \ + * twofish.c twofish_wasm.c + */ + +#include "twofish.h" + +/* ------------------------------------------------------------------------- + * Platform shims — replace the things normally provided by libc. + * clang's compiler-rt provides __builtin_memset/memcpy/memcmp as + * inline builtins, but the C code calls the named functions. + * We provide them ourselves to stay fully nostdlib. + * ------------------------------------------------------------------------- */ + +void *memset(void *dst, int c, unsigned long n) { + unsigned char *p = (unsigned char *)dst; + while (n--) *p++ = (unsigned char)c; + return dst; +} + +void *memcpy(void *dst, const void *src, unsigned long n) { + unsigned char *d = (unsigned char *)dst; + const unsigned char *s = (const unsigned char *)src; + while (n--) *d++ = *s++; + return dst; +} + +int memcmp(const void *a, const void *b, unsigned long n) { + const unsigned char *p = (const unsigned char *)a; + const unsigned char *q = (const unsigned char *)b; + while (n--) { + if (*p != *q) return (int)*p - (int)*q; + p++; q++; + } + return 0; +} + +/* ------------------------------------------------------------------------- + * Fatal handler. + * Twofish_fatal() is called on internal assertion failures. + * We use __builtin_trap() which emits the WASM `unreachable` instruction. + * This raises a RuntimeError on the JS side that can be caught normally. + * ------------------------------------------------------------------------- */ +__attribute__((noreturn)) void Twofish_fatal( const char *msg ) { + __builtin_trap(); +} + + +/* ------------------------------------------------------------------------- + * Session pool. + * + * We maintain a fixed-size pool of Twofish_key structs. + * JavaScript receives an integer handle (0-based index). + * Handle -1 means "invalid / no session". + * + * MAX_SESSIONS can be increased freely — each slot costs ~4.5 kB. + * ------------------------------------------------------------------------- */ + +#define MAX_SESSIONS 16 + +/* Each entry in the pool. */ +typedef struct { + Twofish_key key; /* The expanded key material (~4.5 kB) */ + int in_use; /* 1 if this slot is occupied, 0 if free */ +} Session; + +static Session session_pool[MAX_SESSIONS]; + +/* ------------------------------------------------------------------------- + * Shared I/O buffer. + * + * Rather than passing pointers from JS (which requires extra wrangling), + * we expose a single 32-byte buffer: the caller writes plaintext/ciphertext + * into it and reads the result from it. + * 16 bytes in + 16 bytes out, laid out sequentially. + * + * bytes 0..15 — input block (written by JS before calling encrypt/decrypt) + * bytes 16..31 — output block (written by WASM, read by JS after the call) + * + * This avoids any pointer-marshalling complexity on the JS side. + * ------------------------------------------------------------------------- */ +static unsigned char io_buffer[32]; + +/* ------------------------------------------------------------------------- + * Exported API + * ------------------------------------------------------------------------- */ + +/* + * twofish_init() + * + * Must be called once before any other function. + * Builds the global q-box and MDS tables, then runs the built-in self-tests. + * + * Returns: 1 on success. + * The WASM trap instruction fires if self-tests fail. + */ +__attribute__((visibility("default"))) +int twofish_init(void) { + /* Zero the session pool. */ + memset(session_pool, 0, sizeof(session_pool)); + + /* + * This call: + * 1. Tests platform macros (GET32, PUT32, ROL/ROR, BSWAP, SELECT_BYTE) + * 2. Builds q_table and MDS_table + * 3. Runs test_vectors() — 3 known (key, plaintext, ciphertext) triples + * 4. Runs test_sequences() — 3 × 49-step recurrence tests + * 5. Runs test_odd_sized_keys() + * If anything fails, __builtin_trap() fires. + */ + Twofish_initialise(); + return 1; +} + +/* + * twofish_get_io_buffer() + * + * Returns the linear-memory offset of io_buffer. + * JS uses this to get a Uint8Array view into the right slice of WASM memory. + * + * Call once after init; the address never changes. + */ +__attribute__((visibility("default"))) +unsigned int twofish_get_io_buffer(void) { + return (unsigned int)(unsigned long)io_buffer; +} + +/* + * twofish_create_session(key_ptr, key_len) + * + * Expands a key and stores the result in a free pool slot. + * + * Arguments: + * key_ptr — offset into WASM linear memory where the key bytes live. + * The caller should write the key bytes into io_buffer[0..key_len-1] + * and pass twofish_get_io_buffer() as key_ptr, or manage their + * own memory region. + * key_len — key length in bytes, must be in range 1..32. + * 16 = 128-bit, 24 = 192-bit, 32 = 256-bit. + * Shorter keys are zero-padded to the next standard size. + * + * Returns: session handle (0 .. MAX_SESSIONS-1) on success. + * -1 if no free slot is available. + * -2 if key_len is out of range. + * The WASM trap fires if the key expansion itself detects an error. + */ +__attribute__((visibility("default"))) +int twofish_create_session(unsigned int key_ptr, int key_len) { + int i; + + /* Validate key length. */ + if (key_len < 1 || key_len > 32) { + return -2; + } + + /* Find a free slot. */ + for (i = 0; i < MAX_SESSIONS; i++) { + if (!session_pool[i].in_use) { + break; + } + } + if (i == MAX_SESSIONS) { + return -1; /* Pool exhausted. */ + } + + /* + * key_ptr is an offset into WASM linear memory. + * We cast it directly — in a wasm32 target, pointers are 32-bit + * and linear memory starts at address 0. + */ + Twofish_prepare_key((Twofish_Byte *)(unsigned long)key_ptr, + key_len, + &session_pool[i].key); + + session_pool[i].in_use = 1; + return i; +} + +/* + * twofish_encrypt(handle) + * + * Encrypts the 16 bytes in io_buffer[0..15]. + * Writes the 16-byte ciphertext to io_buffer[16..31]. + * + * Arguments: + * handle — session handle returned by twofish_create_session(). + * + * Returns: 0 on success. + * -1 if handle is invalid. + */ +__attribute__((visibility("default"))) +int twofish_encrypt(int handle) { + if (handle < 0 || handle >= MAX_SESSIONS || !session_pool[handle].in_use) { + return -1; + } + Twofish_encrypt(&session_pool[handle].key, + io_buffer, /* plaintext — first 16 bytes */ + io_buffer + 16); /* ciphertext — second 16 bytes */ + return 0; +} + +/* + * twofish_decrypt(handle) + * + * Decrypts the 16 bytes in io_buffer[0..15]. + * Writes the 16-byte plaintext to io_buffer[16..31]. + * + * Arguments: + * handle — session handle returned by twofish_create_session(). + * + * Returns: 0 on success. + * -1 if handle is invalid. + */ +__attribute__((visibility("default"))) +int twofish_decrypt(int handle) { + if (handle < 0 || handle >= MAX_SESSIONS || !session_pool[handle].in_use) { + return -1; + } + Twofish_decrypt(&session_pool[handle].key, + io_buffer, /* ciphertext — first 16 bytes */ + io_buffer + 16); /* plaintext — second 16 bytes */ + return 0; +} + +/* + * twofish_destroy_session(handle) + * + * Wipes the key material from the session slot and marks it as free. + * Always call this when you are done with a session. + * + * Returns: 0 on success. + * -1 if handle is invalid. + */ +__attribute__((visibility("default"))) +int twofish_destroy_session(int handle) { + if (handle < 0 || handle >= MAX_SESSIONS || !session_pool[handle].in_use) { + return -1; + } + /* Wipe the key material before releasing the slot. */ + memset(&session_pool[handle].key, 0, sizeof(Twofish_key)); + session_pool[handle].in_use = 0; + return 0; +} \ No newline at end of file diff --git a/packages/twofish/src/index.ts b/packages/twofish/src/index.ts new file mode 100644 index 000000000..ff874841f --- /dev/null +++ b/packages/twofish/src/index.ts @@ -0,0 +1 @@ +export * from './twofish'; diff --git a/packages/twofish/src/twofish.ts b/packages/twofish/src/twofish.ts new file mode 100644 index 000000000..22b33e78c --- /dev/null +++ b/packages/twofish/src/twofish.ts @@ -0,0 +1,363 @@ +/* eslint-disable no-bitwise */ +/** + * twofish.ts + * + * TypeScript wrapper for the Twofish WASM module. + * Provides a clean, multi-session API. + * + * Usage: + * import { TwofishModule } from './twofish'; + * + * const tf = await TwofishModule.load('./twofish.wasm'); + * + * const session = tf.createSession(key); // key: Uint8Array, 1–32 bytes + * const ct = tf.encrypt(session, plaintext); // plaintext: Uint8Array, 16 bytes + * const pt = tf.decrypt(session, ct); // ciphertext: Uint8Array, 16 bytes + * tf.destroySession(session); + */ + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +/** Opaque integer handle for an active Twofish key session. */ +export type SessionHandle = number & { readonly __brand: 'TwofishSession' }; + +/** Raw exports produced by the WASM module. */ +interface TwofishWasmExports { + twofish_init(): number; + twofish_get_io_buffer(): number; + twofish_create_session(keyPtr: number, keyLen: number): number; + twofish_encrypt(handle: number): number; + twofish_decrypt(handle: number): number; + twofish_destroy_session(handle: number): number; + memory: WebAssembly.Memory; +} + +// --------------------------------------------------------------------------- +// TwofishModule +// --------------------------------------------------------------------------- + +export class TwofishModule { + private readonly exports: TwofishWasmExports; + private readonly mem: WebAssembly.Memory; + private readonly ioOffset: number; // byte offset of io_buffer in WASM memory + + private readonly memView; + private constructor(exports: TwofishWasmExports) { + this.exports = exports; + this.mem = exports.memory; + this.ioOffset = exports.twofish_get_io_buffer(); + this.memView = new Uint8Array(this.mem.buffer); + } + + // ----------------------------------------------------------------------- + // Factory + // ----------------------------------------------------------------------- + + /** + * Load and initialise the Twofish WASM module. + * + * @param source One of: + * - A URL string or URL object pointing at twofish.wasm + * - A BufferSource (ArrayBuffer / Uint8Array) with the raw WASM bytes + * + * @throws If the WASM cannot be loaded, or if the built-in self-tests fail + * (in which case the WASM module fires an `unreachable` trap). + */ + static async load(source: string | URL | BufferSource): Promise { + let wasmInstance: WebAssembly.Instance; + + // Load as files + if ( + typeof process !== 'undefined' && + typeof process.versions.node !== 'undefined' + ) { + const { readFile } = await import('fs/promises'); + + let pathToRead: string | null = null; + if (typeof source === 'string' && source.startsWith('file://')) { + pathToRead = new URL(source).pathname; + } else if (source instanceof URL && source.protocol === 'file:') { + pathToRead = source.pathname; + } + + if (pathToRead) { + source = await readFile(pathToRead); + } + } + + if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) { + /* We already have the bytes — use the synchronous path. */ + const result = await WebAssembly.instantiate(source as BufferSource); + wasmInstance = result.instance; + } else { + /* URL path — use streaming instantiation when available. */ + if (typeof WebAssembly.instantiateStreaming === 'function') { + const result = await WebAssembly.instantiateStreaming( + fetch(source as string), + ); + wasmInstance = result.instance; + } else { + /* Node.js or environments without streaming. */ + const bytes = await TwofishModule.fetchBytes(source); + const result = await WebAssembly.instantiate(bytes); + wasmInstance = result.instance; + } + } + + const exports = wasmInstance.exports as unknown as TwofishWasmExports; + + /* Run Twofish_initialise() + all built-in self-tests. */ + const ok = exports.twofish_init(); + if (ok !== 1) { + throw new Error('Twofish: init returned unexpected value'); + } + + return new TwofishModule(exports); + } + + // ----------------------------------------------------------------------- + // Session management + // ----------------------------------------------------------------------- + + /** + * Prepare a key and open a new encryption session. + * + * @param key 1–32 bytes. Standard sizes are 16, 24, or 32 bytes. + * Shorter keys are zero-padded to the next standard size + * by the Twofish key schedule. + * + * @returns An opaque SessionHandle. Keep it until you call destroySession(). + * @throws On invalid key length or pool exhaustion. + */ + createSession(key: Uint8Array): SessionHandle { + if (key.length < 1 || key.length > 32) { + throw new RangeError(`Twofish key must be 1–32 bytes, got ${key.length}`); + } + + /* + * We write the key directly into the first key.length bytes of + * io_buffer in WASM memory. This is safe as long as the JS caller + * doesn't interleave a concurrent encrypt/decrypt call during setup + * (which is impossible in a single-threaded JS runtime anyway). + */ + const memView = new Uint8Array(this.mem.buffer); + memView.set(key, this.ioOffset); + + const handle = this.exports.twofish_create_session(this.ioOffset, key.length); + + if (handle === -1) { + throw new Error('Twofish: session pool exhausted (max 16 sessions)'); + } + if (handle === -2) { + throw new RangeError('Twofish: invalid key length reported by WASM'); + } + + /* Wipe the key bytes we just wrote to io_buffer. */ + memView.fill(0, this.ioOffset, this.ioOffset + key.length); + + return handle as SessionHandle; + } + + // TODO: add method to encrypt and decrypt the same buffer + // It would remain a performance high, but prevent unexpected mutations + /** + * Encrypt a single 16-byte block. + * + * **WARNING**: returned buffer is view of WASM memory by performance reasons, + * so buffer may be changed anytime. + * Copy buffer immediately after return via `.slice()` to prevent mutations + * + * @param handle Session handle from createSession(). + * @param plaintext Exactly 16 bytes. + * @returns 16-byte ciphertext as a new Uint8Array. + * @throws On invalid handle or wrong plaintext length. + */ + encrypt(handle: SessionHandle, plaintext: Uint8Array): Uint8Array { + this.validateBlock(plaintext, 'plaintext'); + + /* Write plaintext into io_buffer[0..15]. */ + this.memView.set(plaintext, this.ioOffset); + + const rc = this.exports.twofish_encrypt(handle); + if (rc !== 0) { + throw new Error(`Twofish encrypt failed (rc=${rc}, handle=${handle})`); + } + + /* + * Read ciphertext from io_buffer[16..31]. + * We slice() to get an independent copy — if the WASM memory + * ever grows (re-allocated), the view would be detached. + */ + return new Uint8Array(this.mem.buffer, this.ioOffset + 16, 16); + } + + /** + * Decrypt a single 16-byte block. + * + * **WARNING**: returned buffer is view of WASM memory by performance reasons, + * so buffer may be changed anytime. + * Copy buffer immediately after return via `.slice()` to prevent mutations + * + * @param handle Session handle from createSession(). + * @param ciphertext Exactly 16 bytes. + * @returns 16-byte plaintext as a new Uint8Array. + * @throws On invalid handle or wrong ciphertext length. + */ + decrypt(handle: SessionHandle, ciphertext: Uint8Array): Uint8Array { + this.validateBlock(ciphertext, 'ciphertext'); + + /* Write ciphertext into io_buffer[0..15]. */ + const memView = new Uint8Array(this.mem.buffer); + memView.set(ciphertext, this.ioOffset); + + const rc = this.exports.twofish_decrypt(handle); + if (rc !== 0) { + throw new Error(`Twofish decrypt failed (rc=${rc}, handle=${handle})`); + } + + /* Read plaintext from io_buffer[16..31]. */ + return new Uint8Array(this.mem.buffer, this.ioOffset + 16, 16); + } + + /** + * Wipe key material and release the session slot back to the pool. + * Always call this when you are done with a session. + * + * @throws On invalid handle. + */ + destroySession(handle: SessionHandle): void { + const rc = this.exports.twofish_destroy_session(handle); + if (rc !== 0) { + throw new Error(`Twofish destroySession failed (rc=${rc}, handle=${handle})`); + } + } + + // ----------------------------------------------------------------------- + // Block-mode helpers + // ----------------------------------------------------------------------- + + /** + * Encrypt multiple 16-byte blocks in ECB mode. + * + * @param handle Session handle. + * @param plaintext Must be a multiple of 16 bytes. + * @returns Ciphertext of the same length. + */ + encryptECB(handle: SessionHandle, plaintext: Uint8Array): Uint8Array { + this.validateMultiBlock(plaintext, 'plaintext'); + const out = new Uint8Array(plaintext.length); + for (let i = 0; i < plaintext.length; i += 16) { + out.set(this.encrypt(handle, plaintext.subarray(i, i + 16)), i); + } + return out; + } + + /** + * Decrypt multiple 16-byte blocks in ECB mode. + * + * @param handle Session handle. + * @param ciphertext Must be a multiple of 16 bytes. + * @returns Plaintext of the same length. + */ + decryptECB(handle: SessionHandle, ciphertext: Uint8Array): Uint8Array { + this.validateMultiBlock(ciphertext, 'ciphertext'); + const out = new Uint8Array(ciphertext.length); + for (let i = 0; i < ciphertext.length; i += 16) { + out.set(this.decrypt(handle, ciphertext.subarray(i, i + 16)), i); + } + return out; + } + + /** + * Encrypt multiple 16-byte blocks in CBC mode. + * + * @param handle Session handle. + * @param plaintext Must be a multiple of 16 bytes. + * @param iv Initialisation vector, exactly 16 bytes. + * @returns Ciphertext of the same length as plaintext. + */ + encryptCBC(handle: SessionHandle, plaintext: Uint8Array, iv: Uint8Array): Uint8Array { + this.validateBlock(iv, 'IV'); + this.validateMultiBlock(plaintext, 'plaintext'); + + const out = new Uint8Array(plaintext.length); + let prev = iv.slice(); /* previous ciphertext block (starts as IV) */ + + for (let i = 0; i < plaintext.length; i += 16) { + /* XOR plaintext block with previous ciphertext block. */ + const block = plaintext.subarray(i, i + 16).slice(); + for (let j = 0; j < 16; j++) block[j] ^= prev[j]; + + const ct = this.encrypt(handle, block); + out.set(ct, i); + prev = ct as Uint8Array; + } + return out; + } + + /** + * Decrypt multiple 16-byte blocks in CBC mode. + * + * @param handle Session handle. + * @param ciphertext Must be a multiple of 16 bytes. + * @param iv Initialisation vector, exactly 16 bytes. + * @returns Plaintext of the same length as ciphertext. + */ + decryptCBC( + handle: SessionHandle, + ciphertext: Uint8Array, + iv: Uint8Array, + ): Uint8Array { + this.validateBlock(iv, 'IV'); + this.validateMultiBlock(ciphertext, 'ciphertext'); + + const out = new Uint8Array(ciphertext.length); + let prev = iv.slice(); + + for (let i = 0; i < ciphertext.length; i += 16) { + const block = ciphertext.subarray(i, i + 16); + const pt = this.decrypt(handle, block); + + /* XOR decrypted block with previous ciphertext block. */ + for (let j = 0; j < 16; j++) pt[j] ^= prev[j]; + out.set(pt, i); + prev = block.slice(); + } + return out; + } + + // ----------------------------------------------------------------------- + // Private helpers + // ----------------------------------------------------------------------- + + private validateBlock(data: Uint8Array, name: string): void { + if (data.length !== 16) { + throw new RangeError( + `Twofish ${name} must be exactly 16 bytes, got ${data.length}`, + ); + } + } + + private validateMultiBlock(data: Uint8Array, name: string): void { + if (data.length === 0 || data.length % 16 !== 0) { + throw new RangeError( + `Twofish ${name} must be a non-empty multiple of 16 bytes, got ${data.length}`, + ); + } + } + + /** Simple fetch-to-ArrayBuffer for Node.js / environments without streaming. */ + private static async fetchBytes(source: string | URL): Promise { + /* Node.js 18+ has global fetch. Older Node needs fs.readFile. */ + if (typeof fetch !== 'undefined') { + return (await fetch(source as string)).arrayBuffer(); + } + + /* Node.js fallback using fs/promises */ + const fs = await import('fs/promises'); + const buf = await fs.readFile(source instanceof URL ? source.pathname : source); + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + } +} diff --git a/packages/twofish/tests/testvectors.ts b/packages/twofish/tests/testvectors.ts new file mode 100644 index 000000000..9f02c7ac7 --- /dev/null +++ b/packages/twofish/tests/testvectors.ts @@ -0,0 +1,189 @@ +// The source is https://www.schneier.com/wp-content/uploads/2015/12/ecb_ival.txt +// Found on https://www.schneier.com/academic/twofish/ +export default [ + { + keysize: 128, + tests: [ + { + key: '00000000000000000000000000000000', + pt: '00000000000000000000000000000000', + ct: '9F589F5CF6122C32B6BFEC2F2AE8C35A', + }, + { + key: '00000000000000000000000000000000', + pt: '9F589F5CF6122C32B6BFEC2F2AE8C35A', + ct: 'D491DB16E7B1C39E86CB086B789F5419', + }, + { + key: '9F589F5CF6122C32B6BFEC2F2AE8C35A', + pt: 'D491DB16E7B1C39E86CB086B789F5419', + ct: '019F9809DE1711858FAAC3A3BA20FBC3', + }, + { + key: 'D491DB16E7B1C39E86CB086B789F5419', + pt: '019F9809DE1711858FAAC3A3BA20FBC3', + ct: '6363977DE839486297E661C6C9D668EB', + }, + { + key: '019F9809DE1711858FAAC3A3BA20FBC3', + pt: '6363977DE839486297E661C6C9D668EB', + ct: '816D5BD0FAE35342BF2A7412C246F752', + }, + { + key: '6363977DE839486297E661C6C9D668EB', + pt: '816D5BD0FAE35342BF2A7412C246F752', + ct: '5449ECA008FF5921155F598AF4CED4D0', + }, + { + key: '816D5BD0FAE35342BF2A7412C246F752', + pt: '5449ECA008FF5921155F598AF4CED4D0', + ct: '6600522E97AEB3094ED5F92AFCBCDD10', + }, + { + key: '5449ECA008FF5921155F598AF4CED4D0', + pt: '6600522E97AEB3094ED5F92AFCBCDD10', + ct: '34C8A5FB2D3D08A170D120AC6D26DBFA', + }, + { + key: '6600522E97AEB3094ED5F92AFCBCDD10', + pt: '34C8A5FB2D3D08A170D120AC6D26DBFA', + ct: '28530B358C1B42EF277DE6D4407FC591', + }, + { + key: '34C8A5FB2D3D08A170D120AC6D26DBFA', + pt: '28530B358C1B42EF277DE6D4407FC591', + ct: '8A8AB983310ED78C8C0ECDE030B8DCA4', + }, + { + key: '137A24CA47CD12BE818DF4D2F4355960', + pt: 'BCA724A54533C6987E14AA827952F921', + ct: '6B459286F3FFD28D49F15B1581B08E42', + }, + { + key: 'BCA724A54533C6987E14AA827952F921', + pt: '6B459286F3FFD28D49F15B1581B08E42', + ct: '5D9D4EEFFA9151575524F115815A12E0', + }, + ], + }, + { + keysize: 192, + tests: [ + { + key: '000000000000000000000000000000000000000000000000', + pt: '00000000000000000000000000000000', + ct: 'EFA71F788965BD4453F860178FC19101', + }, + { + key: '000000000000000000000000000000000000000000000000', + pt: 'EFA71F788965BD4453F860178FC19101', + ct: '88B2B2706B105E36B446BB6D731A1E88', + }, + { + key: 'EFA71F788965BD4453F860178FC191010000000000000000', + pt: '88B2B2706B105E36B446BB6D731A1E88', + ct: '39DA69D6BA4997D585B6DC073CA341B2', + }, + { + key: '88B2B2706B105E36B446BB6D731A1E88EFA71F788965BD44', + pt: '39DA69D6BA4997D585B6DC073CA341B2', + ct: '182B02D81497EA45F9DAACDC29193A65', + }, + { + key: '39DA69D6BA4997D585B6DC073CA341B288B2B2706B105E36', + pt: '182B02D81497EA45F9DAACDC29193A65', + ct: '7AFF7A70CA2FF28AC31DD8AE5DAAAB63', + }, + { + key: '182B02D81497EA45F9DAACDC29193A6539DA69D6BA4997D5', + pt: '7AFF7A70CA2FF28AC31DD8AE5DAAAB63', + ct: 'D1079B789F666649B6BD7D1629F1F77E', + }, + { + key: '7AFF7A70CA2FF28AC31DD8AE5DAAAB63182B02D81497EA45', + pt: 'D1079B789F666649B6BD7D1629F1F77E', + ct: '3AF6F7CE5BD35EF18BEC6FA787AB506B', + }, + { + key: 'D1079B789F666649B6BD7D1629F1F77E7AFF7A70CA2FF28A', + pt: '3AF6F7CE5BD35EF18BEC6FA787AB506B', + ct: 'AE8109BFDA85C1F2C5038B34ED691BFF', + }, + { + key: '3AF6F7CE5BD35EF18BEC6FA787AB506BD1079B789F666649', + pt: 'AE8109BFDA85C1F2C5038B34ED691BFF', + ct: '893FD67B98C550073571BD631263FC78', + }, + { + key: 'AE8109BFDA85C1F2C5038B34ED691BFF3AF6F7CE5BD35EF1', + pt: '893FD67B98C550073571BD631263FC78', + ct: '16434FC9C8841A63D58700B5578E8F67', + }, + { + key: 'DEA4F3DA75EC7A8EAC3861A9912402CD5DBE44032769DF54', + pt: 'FB66522C332FCC4C042ABE32FA9E902F', + ct: 'F0AB73301125FA21EF70BE5385FB76B6', + }, + { + key: 'FB66522C332FCC4C042ABE32FA9E902FDEA4F3DA75EC7A8E', + pt: 'F0AB73301125FA21EF70BE5385FB76B6', + ct: 'E75449212BEEF9F4A390BD860A640941', + }, + ], + }, + { + keysize: 256, + tests: [ + { + key: '0000000000000000000000000000000000000000000000000000000000000000', + pt: '00000000000000000000000000000000', + ct: '57FF739D4DC92C1BD7FC01700CC8216F', + }, + { + key: '0000000000000000000000000000000000000000000000000000000000000000', + pt: '57FF739D4DC92C1BD7FC01700CC8216F', + ct: 'D43BB7556EA32E46F2A282B7D45B4E0D', + }, + { + key: '57FF739D4DC92C1BD7FC01700CC8216F00000000000000000000000000000000', + pt: 'D43BB7556EA32E46F2A282B7D45B4E0D', + ct: '90AFE91BB288544F2C32DC239B2635E6', + }, + { + key: 'D43BB7556EA32E46F2A282B7D45B4E0D57FF739D4DC92C1BD7FC01700CC8216F', + pt: '90AFE91BB288544F2C32DC239B2635E6', + ct: '6CB4561C40BF0A9705931CB6D408E7FA', + }, + { + key: '90AFE91BB288544F2C32DC239B2635E6D43BB7556EA32E46F2A282B7D45B4E0D', + pt: '6CB4561C40BF0A9705931CB6D408E7FA', + ct: '3059D6D61753B958D92F4781C8640E58', + }, + { + key: '6CB4561C40BF0A9705931CB6D408E7FA90AFE91BB288544F2C32DC239B2635E6', + pt: '3059D6D61753B958D92F4781C8640E58', + ct: 'E69465770505D7F80EF68CA38AB3A3D6', + }, + { + key: '3059D6D61753B958D92F4781C8640E586CB4561C40BF0A9705931CB6D408E7FA', + pt: 'E69465770505D7F80EF68CA38AB3A3D6', + ct: '5AB67A5F8539A4A5FD9F0373BA463466', + }, + { + key: 'E69465770505D7F80EF68CA38AB3A3D63059D6D61753B958D92F4781C8640E58', + pt: '5AB67A5F8539A4A5FD9F0373BA463466', + ct: 'DC096BCD99FC72F79936D4C748E75AF7', + }, + { + key: '5AB67A5F8539A4A5FD9F0373BA463466E69465770505D7F80EF68CA38AB3A3D6', + pt: 'DC096BCD99FC72F79936D4C748E75AF7', + ct: 'C5A3E7CEE0F1B7260528A68FB4EA05F2', + }, + { + key: 'DC096BCD99FC72F79936D4C748E75AF75AB67A5F8539A4A5FD9F0373BA463466', + pt: 'C5A3E7CEE0F1B7260528A68FB4EA05F2', + ct: '43D5CEC327B24AB90AD34A79D0469151', + }, + ], + }, +]; diff --git a/packages/twofish/tests/twofish.errors.test.ts b/packages/twofish/tests/twofish.errors.test.ts new file mode 100644 index 000000000..a27f5d5eb --- /dev/null +++ b/packages/twofish/tests/twofish.errors.test.ts @@ -0,0 +1,72 @@ +/* eslint-disable import/no-unresolved */ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { TwofishModule } from '../dist'; + +let tf: TwofishModule; + +beforeAll(async () => { + tf = await TwofishModule.load( + readFileSync(resolve(__dirname, '../dist/twofish.wasm')), + ); +}); + +// ----------------------------------------------------------------------- +// Invalid key length — twofish_create_session returns -2 +// ----------------------------------------------------------------------- +test('createSession throws on zero-length key', () => { + expect(() => tf.createSession(new Uint8Array(0))).toThrow(); +}); + +test('createSession throws on key longer than 32 bytes', () => { + expect(() => tf.createSession(new Uint8Array(33))).toThrow(); +}); + +// ----------------------------------------------------------------------- +// Session pool exhaustion — twofish_create_session returns -1 +// MAX_SESSIONS = 16 +// ----------------------------------------------------------------------- +test('createSession throws when all 16 session slots are occupied', () => { + const sessions: ReturnType[] = []; + const key = new Uint8Array(32).fill(0xab); + + try { + for (let i = 0; i < 16; i++) { + sessions.push(tf.createSession(key)); + } + // The 17th attempt must throw (pool is full) + expect(() => tf.createSession(key)).toThrow(); + } finally { + // Always clean up so we don't poison other tests + sessions.forEach((s) => tf.destroySession(s)); + } +}); + +// ----------------------------------------------------------------------- +// Operations on a destroyed (invalid) handle +// twofish_encrypt / twofish_decrypt / twofish_destroy_session return -1 +// ----------------------------------------------------------------------- +test('encrypt throws when called with a destroyed session handle', () => { + const key = new Uint8Array(32).fill(0x01); + const session = tf.createSession(key); + tf.destroySession(session); + + expect(() => tf.encrypt(session, new Uint8Array(16))).toThrow(); +}); + +test('decrypt throws when called with a destroyed session handle', () => { + const key = new Uint8Array(32).fill(0x02); + const session = tf.createSession(key); + tf.destroySession(session); + + expect(() => tf.decrypt(session, new Uint8Array(16))).toThrow(); +}); + +test('destroySession throws when called twice on the same handle', () => { + const key = new Uint8Array(32).fill(0x03); + const session = tf.createSession(key); + tf.destroySession(session); + + expect(() => tf.destroySession(session)).toThrow(); +}); diff --git a/packages/twofish/tests/twofish.vectors.test.ts b/packages/twofish/tests/twofish.vectors.test.ts new file mode 100644 index 000000000..3e4a4b88b --- /dev/null +++ b/packages/twofish/tests/twofish.vectors.test.ts @@ -0,0 +1,46 @@ +/* eslint-disable import/no-unresolved */ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { TwofishModule } from '../dist'; +import testvectors from './testvectors'; + +function fromHex(str: string) { + const l = str.length / 2; + const out = new Uint8Array(l); + for (let i = 0; i < l; i++) { + out[i] = parseInt(str.substr(2 * i, 2), 16); + } + return out; +} + +function toHex(buf: Uint8Array) { + return [...buf] + .map((n) => { + const h = n.toString(16); + return h.length === 1 ? '0' + h : h; + }) + .join('') + .toUpperCase(); +} + +let tf: TwofishModule; +beforeAll(async () => { + tf = await TwofishModule.load( + readFileSync(resolve(__dirname, '../dist/twofish.wasm')), + ); +}); + +testvectors.forEach(({ keysize, tests }) => + describe(`Key size ${keysize}`, () => { + tests.forEach((data) => + test(`Encrypt pt=${data.pt} with key=${data.key}`, () => { + const session = tf.createSession(fromHex(data.key)); + onTestFinished(() => tf.destroySession(session)); + + const result = tf.encrypt(session, fromHex(data.pt)); + expect(toHex(result)).toBe(data.ct); + }), + ); + }), +); diff --git a/packages/twofish/tsconfig.json b/packages/twofish/tsconfig.json new file mode 100644 index 000000000..29046a64a --- /dev/null +++ b/packages/twofish/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "module": "esnext", + "moduleResolution": "bundler", + "outDir": "dist", + "target": "esnext", + "declaration": true, + "esModuleInterop": true, + "strict": true, + "alwaysStrict": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "downlevelIteration": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "pretty": true, + "preserveConstEnums": true, + "removeComments": false, + "sourceMap": true, + "rootDir": ".", + "allowJs": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "baseUrl": ".", + "types": ["vitest/globals", "node"] + }, + "include": ["*.ts", "**/*.ts"] +} diff --git a/packages/twofish/tsdown.config.ts b/packages/twofish/tsdown.config.ts new file mode 100644 index 000000000..14f50f263 --- /dev/null +++ b/packages/twofish/tsdown.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + clean: false, + dts: true, + exports: { + customExports: { + './twofish.wasm': './dist/twofish.wasm', + }, + }, +}); diff --git a/scripts/webpack/app.ts b/scripts/webpack/app.ts index 55e491fb1..3d67667a4 100644 --- a/scripts/webpack/app.ts +++ b/scripts/webpack/app.ts @@ -45,6 +45,7 @@ export default merge(commonConfig, { fallback: { // eslint-disable-next-line camelcase worker_threads: false, + 'fs/promises': false, }, }, experiments: { diff --git a/src/core/encryption/__tests__/__snapshots__/ciphers.vectors.test.ts.snap b/src/core/encryption/__tests__/__snapshots__/ciphers.vectors.test.ts.snap new file mode 100644 index 000000000..3a5509fa7 --- /dev/null +++ b/src/core/encryption/__tests__/__snapshots__/ciphers.vectors.test.ts.snap @@ -0,0 +1,481 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`AES > Cipher must match reference test vectors cascade 1`] = ` +{ + "ct": "000000000000000000000000000000000000000000000000000000000000000007AC381616003CD4ABAA7586FB547AE1", + "key": "0000000000000000000000000000000000000000000000000000000000000000", + "pt": "00000000000000000000000000000000", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 2`] = ` +{ + "ct": "000000000000000000000000000000000000000000000000000000000000000007AC381616003CD4ABAA7586FB547AE1C1B9E1D358B215F3E401F799EA070EFA0CD8D65A06C3FA08B330239170C20D1E", + "key": "0000000000000000000000000000000000000000000000000000000000000000", + "pt": "000000000000000000000000000000000000000000000000000000000000000007AC381616003CD4ABAA7586FB547AE1", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 3`] = ` +{ + "ct": "00000000000000000000000000000000000000000000000000000000000000003CDB243E9949984F3F128D612E36B9AACC387C9EED27836C6C411E7E7FA63D1435309B7DCEA0AAF1E8B3F2C73F3854834E2AADCA36082CF22B3C11AF1FB02FF0B33CD90F0D794D747EE5324EBF53068C", + "key": "0000000000000000000000000000000007AC381616003CD4ABAA7586FB547AE1", + "pt": "000000000000000000000000000000000000000000000000000000000000000007AC381616003CD4ABAA7586FB547AE1C1B9E1D358B215F3E401F799EA070EFA0CD8D65A06C3FA08B330239170C20D1E", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 4`] = ` +{ + "ct": "00000000000000000000000000000000000000000000000000000000000000003CDB243E9949984F3F128D612E36B9AACC387C9EED27836C6C411E7E7FA63D140E47875541E90E6A7C0B0A20EA5A97C843AB3087839DBA6DA37CF8488A111C1E8AD49428C51A1D8D2566E318F0A95F11CC71A561236E95F87F8235AEBE5FA0433298CCECEDDBEC28F5D9F9898DD6F07E", + "key": "0000000000000000000000000000000007AC381616003CD4ABAA7586FB547AE1", + "pt": "00000000000000000000000000000000000000000000000000000000000000003CDB243E9949984F3F128D612E36B9AACC387C9EED27836C6C411E7E7FA63D1435309B7DCEA0AAF1E8B3F2C73F3854834E2AADCA36082CF22B3C11AF1FB02FF0B33CD90F0D794D747EE5324EBF53068C", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 5`] = ` +{ + "ct": "00000000000000000000000000000000000000000000000000000000000000006ACA67C863D28F993804B1E4D4C95677339983BC377534FCF9EE92A57F56E3EF85824BABFA62B581A86BDD59BDDE0C65A24E058FF1F972D96D0714B2EADAE5DB2BB5BD5BD05CA0F0418D4AA6D7973ECA535653DB4077CC079C80D49EEE4BC34D9ABA4BFDD6DE7014551833C6D033A92D0F590D444A3BE46821D967EBC0CC3929EBFF27EBA506AAF16B85F2A718703F06", + "key": "4E2AADCA36082CF22B3C11AF1FB02FF0B33CD90F0D794D747EE5324EBF53068C", + "pt": "00000000000000000000000000000000000000000000000000000000000000003CDB243E9949984F3F128D612E36B9AACC387C9EED27836C6C411E7E7FA63D140E47875541E90E6A7C0B0A20EA5A97C843AB3087839DBA6DA37CF8488A111C1E8AD49428C51A1D8D2566E318F0A95F11CC71A561236E95F87F8235AEBE5FA0433298CCECEDDBEC28F5D9F9898DD6F07E", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 6`] = ` +{ + "ct": "00000000000000000000000000000000000000000000000000000000000000006ACA67C863D28F993804B1E4D4C95677339983BC377534FCF9EE92A57F56E3EFD393085D00F9A257AF7DE1DC4721E3B85DEFFAAD2BABC549F8A89869EA2A3B20A07071A56BD71B1B95ED9DDF8013A567B2B366D3321304B352FB38648E803A883BDB628EC398CD6931F39A78F70DC8F6907EFBFE2922BD97C2DB86DB90D85A2743DDA0FA9E0336CDCB4438E845956655EA0308A951A12BC3C5284012DEEB1E82EB87E708E7E0B86F531590CC6DEF25FE", + "key": "4E2AADCA36082CF22B3C11AF1FB02FF0B33CD90F0D794D747EE5324EBF53068C", + "pt": "00000000000000000000000000000000000000000000000000000000000000006ACA67C863D28F993804B1E4D4C95677339983BC377534FCF9EE92A57F56E3EF85824BABFA62B581A86BDD59BDDE0C65A24E058FF1F972D96D0714B2EADAE5DB2BB5BD5BD05CA0F0418D4AA6D7973ECA535653DB4077CC079C80D49EEE4BC34D9ABA4BFDD6DE7014551833C6D033A92D0F590D444A3BE46821D967EBC0CC3929EBFF27EBA506AAF16B85F2A718703F06", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 7`] = ` +{ + "ct": "000000000000000000000000000000000000000000000000000000000000000054D361597DCA07DBD81EDDDC59EF835E231B0BC13E9F4743064C4C865F43CB4E085265552B28A513AF2C1AB4FFC154B96725F53F1CB34D167E81184DAACD3FBD34F2264783E15DD54483909EC7684B92622484FF9528C74540688E605365CD426B9B3228C2D0FD37067CA689A135ED5BCB6FA2D694F9B6CBE155B466E08BA948BFB14D81456572D164EC6CF374507E723092CB7F3882CE5FCBB3574440A9A8CE5B433E39A844F14B6994CDB3F0B92F13AEBB3A634118A038C7A7479AF278AE25EB24115C225BC22656101F98A790F42D", + "key": "0F590D444A3BE46821D967EBC0CC3929EBFF27EBA506AAF16B85F2A718703F06", + "pt": "00000000000000000000000000000000000000000000000000000000000000006ACA67C863D28F993804B1E4D4C95677339983BC377534FCF9EE92A57F56E3EFD393085D00F9A257AF7DE1DC4721E3B85DEFFAAD2BABC549F8A89869EA2A3B20A07071A56BD71B1B95ED9DDF8013A567B2B366D3321304B352FB38648E803A883BDB628EC398CD6931F39A78F70DC8F6907EFBFE2922BD97C2DB86DB90D85A2743DDA0FA9E0336CDCB4438E845956655EA0308A951A12BC3C5284012DEEB1E82EB87E708E7E0B86F531590CC6DEF25FE", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 8`] = ` +{ + "ct": "000000000000000000000000000000000000000000000000000000000000000054D361597DCA07DBD81EDDDC59EF835E231B0BC13E9F4743064C4C865F43CB4E364B63C435302D514F36768C72E7819077A77D4215593EA98123C66E8AD8171CEF334B4FA8305A9144D26BF67F88FC9358EE8B6DA2304F1AC6410E441382C9DFFF1965CA2AE6BBF9D712ABC8E64E03AE1BF840FA33C2753DF3C602623D6E5E82EFF11D27442D428F5363500222685BDF6B8392578559C503E83D65F930FA5BA1A72FD3427322B557C63C99A8C17C3734742AF9B5283B45A4C93C50CC6C3A18695BE0C86D6DFF8B026C9142E73AC6FEC05F5EB8DE98E866998AA06E664E52630C025FFA69542D5A154C92439DF0F60B81", + "key": "0F590D444A3BE46821D967EBC0CC3929EBFF27EBA506AAF16B85F2A718703F06", + "pt": "000000000000000000000000000000000000000000000000000000000000000054D361597DCA07DBD81EDDDC59EF835E231B0BC13E9F4743064C4C865F43CB4E085265552B28A513AF2C1AB4FFC154B96725F53F1CB34D167E81184DAACD3FBD34F2264783E15DD54483909EC7684B92622484FF9528C74540688E605365CD426B9B3228C2D0FD37067CA689A135ED5BCB6FA2D694F9B6CBE155B466E08BA948BFB14D81456572D164EC6CF374507E723092CB7F3882CE5FCBB3574440A9A8CE5B433E39A844F14B6994CDB3F0B92F13AEBB3A634118A038C7A7479AF278AE25EB24115C225BC22656101F98A790F42D", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 9`] = ` +{ + "ct": "0000000000000000000000000000000000000000000000000000000000000000A3EEBAC38C434D706A062322FFBB447F06F4E1C3AD467D66918E3AF4EEA974D1D745C420EF0AE3C6195545C74F4C31B8E31DAE64CCD15D7AA1148EEC3A36FFDCC8B4AAFA7E5990CF300643EFE45A52E27B0CDE4E0A3FA631B8ADD8F640F6B96862731FB421ED9CFA30CEB71695659FE15ABF12BC7D4B60FFC9E887534D8B90AA03867DA939C26428A9F58BD4E575563BAA92B23C603C0695FBB8D4513E6C7A0194E28B36C458D8BB477298427EB4E5BEAC7E8F1E1722717CE05C27025DF3C37FCCDEA981B1BBBB9F3A55F13CC5B9FF4D33557A529268A1EF56367FDEE1C8697C9FD86611E3FD008B5BE20BC6EA34F25446BC68B5E91B97BA28464C00CB0D9EBEE3B98BFDC361E711D81BB8359A33A661", + "key": "AEBB3A634118A038C7A7479AF278AE25EB24115C225BC22656101F98A790F42D", + "pt": "000000000000000000000000000000000000000000000000000000000000000054D361597DCA07DBD81EDDDC59EF835E231B0BC13E9F4743064C4C865F43CB4E364B63C435302D514F36768C72E7819077A77D4215593EA98123C66E8AD8171CEF334B4FA8305A9144D26BF67F88FC9358EE8B6DA2304F1AC6410E441382C9DFFF1965CA2AE6BBF9D712ABC8E64E03AE1BF840FA33C2753DF3C602623D6E5E82EFF11D27442D428F5363500222685BDF6B8392578559C503E83D65F930FA5BA1A72FD3427322B557C63C99A8C17C3734742AF9B5283B45A4C93C50CC6C3A18695BE0C86D6DFF8B026C9142E73AC6FEC05F5EB8DE98E866998AA06E664E52630C025FFA69542D5A154C92439DF0F60B81", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 10`] = ` +{ + "ct": "0000000000000000000000000000000000000000000000000000000000000000A3EEBAC38C434D706A062322FFBB447F06F4E1C3AD467D66918E3AF4EEA974D120781FBA1E83A96DAB4DBB39E918F699C6F244665F08675F36D6F89E8BDC404329BA0D1EA4635E58666570A4D9F1E2CAEFB60D68D3B7C5E2989A9074F01851A845F4FE01F78456A4441A9F0F0EB73190795D479FD54489D4B70451E11EFFE01D9EEC07D732C9432B4E29970A965ECA74EBD5E07A2EB51357C19651604E89B4297895EBB8B9B7FE1CBDE44394B9A9E85A6D6FAF75F247B2EAF3D996AA5365E2DFFF13F1F506C1D673BB1BF0D67A712DC7EB010CF9AD7195377F560810D001B26A08E607FD3FB930160D26B81D154BF3D92AB7AA39E39B50CCF4D05DB8649794CE7E3E178574B1BD8FCF6BF06E80F15FB459130FFBE69350670051D0FD91D58582A189D88625E174BBAD7BEF30894021A1", + "key": "AEBB3A634118A038C7A7479AF278AE25EB24115C225BC22656101F98A790F42D", + "pt": "0000000000000000000000000000000000000000000000000000000000000000A3EEBAC38C434D706A062322FFBB447F06F4E1C3AD467D66918E3AF4EEA974D1D745C420EF0AE3C6195545C74F4C31B8E31DAE64CCD15D7AA1148EEC3A36FFDCC8B4AAFA7E5990CF300643EFE45A52E27B0CDE4E0A3FA631B8ADD8F640F6B96862731FB421ED9CFA30CEB71695659FE15ABF12BC7D4B60FFC9E887534D8B90AA03867DA939C26428A9F58BD4E575563BAA92B23C603C0695FBB8D4513E6C7A0194E28B36C458D8BB477298427EB4E5BEAC7E8F1E1722717CE05C27025DF3C37FCCDEA981B1BBBB9F3A55F13CC5B9FF4D33557A529268A1EF56367FDEE1C8697C9FD86611E3FD008B5BE20BC6EA34F25446BC68B5E91B97BA28464C00CB0D9EBEE3B98BFDC361E711D81BB8359A33A661", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 11`] = ` +{ + "ct": "0000000000000000000000000000000000000000000000000000000000000000ABBD0E320EED11FDB544EC30B2A8428F46D066AC66C90DF8B297EF333213764EE76615E3CE552C12D558708822A1D1603189C8F7C682D8D784A03DACB6D713145CED8248BC29EC5B061160EA26A380A87BF2CE619D52007D1B5D3EF785A27B2A10716209177B4EDE7FA3777512A383609C557E9EB24CFE230D306AA63C44A0673E976E43DAF7810C8BFCC9001CA635D6E526B1329D510423AF3B4CF6835C6E376730152AC362581861E8A1AD1D229F2EF3782637B12373E28B8B92A1AA898132B6089A41068EB977FA36D0758A33379DB9CFF1581CC73CB3C1DB1D0196F4A74FCC553AC03AD3BB45BE664FA4AC04AB1EA9074AD1DF917AC524F521AB7415B56C3E7EB111BB8832737AAF7D11617D819F230F195CEB3C86A8AD7C336C4ECDEF32065F36C58CA0438450E755EFBADA9724FA1098212C40CEEAABDB84C03136388F225A6E7856C4CDB800F424E152382F82", + "key": "46BC68B5E91B97BA28464C00CB0D9EBEE3B98BFDC361E711D81BB8359A33A661", + "pt": "0000000000000000000000000000000000000000000000000000000000000000A3EEBAC38C434D706A062322FFBB447F06F4E1C3AD467D66918E3AF4EEA974D120781FBA1E83A96DAB4DBB39E918F699C6F244665F08675F36D6F89E8BDC404329BA0D1EA4635E58666570A4D9F1E2CAEFB60D68D3B7C5E2989A9074F01851A845F4FE01F78456A4441A9F0F0EB73190795D479FD54489D4B70451E11EFFE01D9EEC07D732C9432B4E29970A965ECA74EBD5E07A2EB51357C19651604E89B4297895EBB8B9B7FE1CBDE44394B9A9E85A6D6FAF75F247B2EAF3D996AA5365E2DFFF13F1F506C1D673BB1BF0D67A712DC7EB010CF9AD7195377F560810D001B26A08E607FD3FB930160D26B81D154BF3D92AB7AA39E39B50CCF4D05DB8649794CE7E3E178574B1BD8FCF6BF06E80F15FB459130FFBE69350670051D0FD91D58582A189D88625E174BBAD7BEF30894021A1", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 12`] = ` +{ + "ct": "0000000000000000000000000000000000000000000000000000000000000000ABBD0E320EED11FDB544EC30B2A8428F46D066AC66C90DF8B297EF333213764EEF35A1124CFB709F0A1ABF9A6FB2D79071AD4F980D0DA849A7B9E86B6A6D118B9BF388116CFF69247804AB5BED1AA7518C8942F004D8BFF5A92BFBC5B8A9287D6526ED5F0F31FCDD1FD7673BEDF1E1020811BD97FCA93BBC8EF7C42549FE8AE56B12F24B3A089976B045217A00B28726002E8833FA5973D4150F77B1A1E72E4DC74B7CBE2B5C9A3FA43DFFA797DA608CFD8B777F02C76496E5268F37675C5B2CA9AD64D37C5B1F73263A324C2EB840E927D8781A5FA3FDBBB989190A6F18C4A2854E51743A9CD441FF4B6F075C46B144FBC9B7706E27D3419A7834BA32E0A049FACD8C2CBEE2B920C9EF8AA8D832D958A0BFF9B4D736ACA17D594F7F5E4FCE90461F90514399CC78E523D8905B56490F800C8E8621EF182506F66751EE2E523F858C803BFF85FA87FD689E3E61A299077D7991075B924147EE4CA2359FDC38B1FB0C1C26B367F519A774BCFFA25D4AEA", + "key": "46BC68B5E91B97BA28464C00CB0D9EBEE3B98BFDC361E711D81BB8359A33A661", + "pt": "0000000000000000000000000000000000000000000000000000000000000000ABBD0E320EED11FDB544EC30B2A8428F46D066AC66C90DF8B297EF333213764EE76615E3CE552C12D558708822A1D1603189C8F7C682D8D784A03DACB6D713145CED8248BC29EC5B061160EA26A380A87BF2CE619D52007D1B5D3EF785A27B2A10716209177B4EDE7FA3777512A383609C557E9EB24CFE230D306AA63C44A0673E976E43DAF7810C8BFCC9001CA635D6E526B1329D510423AF3B4CF6835C6E376730152AC362581861E8A1AD1D229F2EF3782637B12373E28B8B92A1AA898132B6089A41068EB977FA36D0758A33379DB9CFF1581CC73CB3C1DB1D0196F4A74FCC553AC03AD3BB45BE664FA4AC04AB1EA9074AD1DF917AC524F521AB7415B56C3E7EB111BB8832737AAF7D11617D819F230F195CEB3C86A8AD7C336C4ECDEF32065F36C58CA0438450E755EFBADA9724FA1098212C40CEEAABDB84C03136388F225A6E7856C4CDB800F424E152382F82", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 13`] = ` +{ + "ct": "000000000000000000000000000000000000000000000000000000000000000058BE35902649160DF2C5F23B7A2ADADFDB0FD91042986987376471129F60577E2DE47F4937CA822737FF99602E654B30F73E30330731202512CBA3CC2A3EB4607B27CACA4771103653AA3C586432BD47F679647F6B4ECCFBB65F6F54CCE3605154FB0D56F84D724D17982164EB1C329D1A55EB8FD4F19CFF348E26C9DB200C91D40F23B9568A8AFF8633E512839C6CB11D55C463CE57139BD0A3634701CFB110502631D8A398F2A9EDA34FCB543BE1CBC316BB578F8B6EBF3C9DCB453F5D2485FAE552E11B6F2404293C11DBC469192B3B238597B740481B19509B5FA1584B199E0B6DDA905652DD3A99FFEE456598E3481D7DA511875B92B91A5CD6F0AB078A842564E612868FE2BDF4D9FB069291C10150A24AC5A5D9988F13EE1D632A0592A8F2B7D6EB71FB651F59C8CA23C2299685378DA0B5D0FCFE4F0B1398C6C9911AF28015C35FDC9172D0F49403E9CFB49F4C88F27FE5153666710CD20FFA1E6E265C8228524105BD2B1B4491B7373EDC8507F2503A9E75195B051E4FBF338D0083916429BFBBE3CC6C8987F7029686C3FC", + "key": "FA1098212C40CEEAABDB84C03136388F225A6E7856C4CDB800F424E152382F82", + "pt": "0000000000000000000000000000000000000000000000000000000000000000ABBD0E320EED11FDB544EC30B2A8428F46D066AC66C90DF8B297EF333213764EEF35A1124CFB709F0A1ABF9A6FB2D79071AD4F980D0DA849A7B9E86B6A6D118B9BF388116CFF69247804AB5BED1AA7518C8942F004D8BFF5A92BFBC5B8A9287D6526ED5F0F31FCDD1FD7673BEDF1E1020811BD97FCA93BBC8EF7C42549FE8AE56B12F24B3A089976B045217A00B28726002E8833FA5973D4150F77B1A1E72E4DC74B7CBE2B5C9A3FA43DFFA797DA608CFD8B777F02C76496E5268F37675C5B2CA9AD64D37C5B1F73263A324C2EB840E927D8781A5FA3FDBBB989190A6F18C4A2854E51743A9CD441FF4B6F075C46B144FBC9B7706E27D3419A7834BA32E0A049FACD8C2CBEE2B920C9EF8AA8D832D958A0BFF9B4D736ACA17D594F7F5E4FCE90461F90514399CC78E523D8905B56490F800C8E8621EF182506F66751EE2E523F858C803BFF85FA87FD689E3E61A299077D7991075B924147EE4CA2359FDC38B1FB0C1C26B367F519A774BCFFA25D4AEA", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 14`] = ` +{ + "ct": "000000000000000000000000000000000000000000000000000000000000000058BE35902649160DF2C5F23B7A2ADADFDB0FD91042986987376471129F60577EDEE744EB1F6E85D7707E876BE6E7D3606AE18F8F2360445A97383DED874D9550B9F614913C40E28E6E4F1AA225E521E770EA1BD461724497032D24F38CB0C5BAB42F4F8DD3C30B5F3C36B6676234288B60A5CD00BB67EFF12BFAB258AF6A44BDE5D2C3B0A1F6046F8E7CA34D8571BF2E0F11927BE60FB4D86ADA81AB93113764EF3BE02ACF1AE120DBD58BA3D7150A5CDE6DF707BB850EF0F931DFB39F75BBD86D881F8793AB4C9260A2A1B70788986C05BE49BF3A0C4232C0EBDF2DF95934B0CD435BE8F76269AA359FDC79AFB4C12154E68028F964EE3219C3DE833EEB88319F605848B84C097E782649121FB1B866B284689FBA05514BAC718671A161A251D61A5F1C4715CDA76B429B99FD62610F24D8D65EA74389C7BD41B2FAFBAC5A181C6D3244F734A66F2A8E8459915BD40649B3F159712AD2BD38F1A6C6D2F9AD032B8EBDAAE15CD6DE36D89B8ABF53F11D3603334220F26E7A9A5E3F85564F561436EA1DCB4981845E35B7DA4A03E5559303A6656733DC9EB7C3A51E814AD29441F688EB66F29C9AE0D77211876763F5CB", + "key": "FA1098212C40CEEAABDB84C03136388F225A6E7856C4CDB800F424E152382F82", + "pt": "000000000000000000000000000000000000000000000000000000000000000058BE35902649160DF2C5F23B7A2ADADFDB0FD91042986987376471129F60577E2DE47F4937CA822737FF99602E654B30F73E30330731202512CBA3CC2A3EB4607B27CACA4771103653AA3C586432BD47F679647F6B4ECCFBB65F6F54CCE3605154FB0D56F84D724D17982164EB1C329D1A55EB8FD4F19CFF348E26C9DB200C91D40F23B9568A8AFF8633E512839C6CB11D55C463CE57139BD0A3634701CFB110502631D8A398F2A9EDA34FCB543BE1CBC316BB578F8B6EBF3C9DCB453F5D2485FAE552E11B6F2404293C11DBC469192B3B238597B740481B19509B5FA1584B199E0B6DDA905652DD3A99FFEE456598E3481D7DA511875B92B91A5CD6F0AB078A842564E612868FE2BDF4D9FB069291C10150A24AC5A5D9988F13EE1D632A0592A8F2B7D6EB71FB651F59C8CA23C2299685378DA0B5D0FCFE4F0B1398C6C9911AF28015C35FDC9172D0F49403E9CFB49F4C88F27FE5153666710CD20FFA1E6E265C8228524105BD2B1B4491B7373EDC8507F2503A9E75195B051E4FBF338D0083916429BFBBE3CC6C8987F7029686C3FC", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 15`] = ` +{ + "ct": "0000000000000000000000000000000000000000000000000000000000000000C6E10E44CCA44E21CB02EDC1E7637F3D161CA1F0FADA875251E3FB56AD9AD7C287FA290BC9F173051DB65D7AB3760638461304F3F22CDC945C69446A6099F25CFB629FF3F2422EE1A606806BB76801BE8F29E58FAC765A8635BE89925FFE26A8C87CAA86CB9D991F271D7F3E01A4BBEF54D3A62E51BE10B3FF4F9F87558ACFE1FF2DEF5B866A517C7F65F0533E6E48EA601706EC9BEE802A2AEA52E36C242BFE037A9275BE007EC5B83ADF35145CFAA3C325DBCEF32D1E01828D6E36A306953F08BA656362850AE35DA9F190FC39BE117D0D85F32ED5775C8EA616BD205682CC73C7CDB34322C87299EB809459DE63010133A2AD6510A7504D5BF24DCB789BF195D2238BD9DD9C7AED8C520A387D2B9A728AEC14F7FBD127BE49CB0468DB9F59C072277EEC58AF55B096725374FF0232F800C1FBDA9E16A7AD65DC16714875264E736422EE3FB6C8CDDC452CC541D1315A9C372F34F75695735D924B0E57F09AB17B19AE5C0E819ACD7A378AA24C96D7FF848C50FD9EDD50B135FF6B52CDC3CC9CA63EF61E49763A5A162479BC0798361BC21DC8CAB87B9379BDFDB8AEEBA4AEB2FD66B88E182643A93FE304BE7780DCAA736CBDD170C07988E27F03DBF05032CD6E8E59EA1CEE1895A4C800B42F7714", + "key": "07F2503A9E75195B051E4FBF338D0083916429BFBBE3CC6C8987F7029686C3FC", + "pt": "000000000000000000000000000000000000000000000000000000000000000058BE35902649160DF2C5F23B7A2ADADFDB0FD91042986987376471129F60577EDEE744EB1F6E85D7707E876BE6E7D3606AE18F8F2360445A97383DED874D9550B9F614913C40E28E6E4F1AA225E521E770EA1BD461724497032D24F38CB0C5BAB42F4F8DD3C30B5F3C36B6676234288B60A5CD00BB67EFF12BFAB258AF6A44BDE5D2C3B0A1F6046F8E7CA34D8571BF2E0F11927BE60FB4D86ADA81AB93113764EF3BE02ACF1AE120DBD58BA3D7150A5CDE6DF707BB850EF0F931DFB39F75BBD86D881F8793AB4C9260A2A1B70788986C05BE49BF3A0C4232C0EBDF2DF95934B0CD435BE8F76269AA359FDC79AFB4C12154E68028F964EE3219C3DE833EEB88319F605848B84C097E782649121FB1B866B284689FBA05514BAC718671A161A251D61A5F1C4715CDA76B429B99FD62610F24D8D65EA74389C7BD41B2FAFBAC5A181C6D3244F734A66F2A8E8459915BD40649B3F159712AD2BD38F1A6C6D2F9AD032B8EBDAAE15CD6DE36D89B8ABF53F11D3603334220F26E7A9A5E3F85564F561436EA1DCB4981845E35B7DA4A03E5559303A6656733DC9EB7C3A51E814AD29441F688EB66F29C9AE0D77211876763F5CB", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 16`] = ` +{ + "ct": "0000000000000000000000000000000000000000000000000000000000000000C6E10E44CCA44E21CB02EDC1E7637F3D161CA1F0FADA875251E3FB56AD9AD7C219A512DF231C2B29247142802E3FA3DA8B007C134A6E32413AEECE2E526372E0A27FF21324DDD833CBCE5A7AE2F9D4E6A3DB6EF37D3AC248FEEFF015B82A41A48AE821E4059F5570EF54E5F793299BB6AB1058759CBA0EA2C9DC32E686C42CF3837E0A509E34C33C644E390A5DFEDB8E54616DC271377F68FE5F7F3C96C4A0A21985BE9E999C2BD649238C2BAF430D67AC234F598ECC2AF3C2BDBD7E5C3389A5E4FB173C139F95063E46A5063F704EEE6045A93A667D67ADF51AA7381C25AC2B16F5B757B20C8E03A4E0D0B3A26F457C79806EE171C9923E03163BDD12772D8D2B56B5D06D9D3DA241F80EE7CE1789BA275FCE916B8F9845EAD1E7CA9D488C99CAC05CBD8DC93A51253C694B533391CE380E4570976096CBBF5D9163B8F2482E581B1C404572D43A1608ACE64CDCB20C8644208A492AC9F56379FCA784B3DFA4E3654FC84505913D2A28F6FFF65693E0ECAB4A26B8435978FA99CBE68E639E5506539AF2A31B217EA1B48879A118FFFCD245A2DA17D4C8B952D63D56AA69317618B14585D9D0D427C69E1D3701954D79B21714122814255D32FA9C3A3FC960DD891B0387969852BBEBE93A836D3B0203FA3D17D366CD7B04905605106F70AB32F3638DD05C0FBBF0C9BA92069A5948F8", + "key": "07F2503A9E75195B051E4FBF338D0083916429BFBBE3CC6C8987F7029686C3FC", + "pt": "0000000000000000000000000000000000000000000000000000000000000000C6E10E44CCA44E21CB02EDC1E7637F3D161CA1F0FADA875251E3FB56AD9AD7C287FA290BC9F173051DB65D7AB3760638461304F3F22CDC945C69446A6099F25CFB629FF3F2422EE1A606806BB76801BE8F29E58FAC765A8635BE89925FFE26A8C87CAA86CB9D991F271D7F3E01A4BBEF54D3A62E51BE10B3FF4F9F87558ACFE1FF2DEF5B866A517C7F65F0533E6E48EA601706EC9BEE802A2AEA52E36C242BFE037A9275BE007EC5B83ADF35145CFAA3C325DBCEF32D1E01828D6E36A306953F08BA656362850AE35DA9F190FC39BE117D0D85F32ED5775C8EA616BD205682CC73C7CDB34322C87299EB809459DE63010133A2AD6510A7504D5BF24DCB789BF195D2238BD9DD9C7AED8C520A387D2B9A728AEC14F7FBD127BE49CB0468DB9F59C072277EEC58AF55B096725374FF0232F800C1FBDA9E16A7AD65DC16714875264E736422EE3FB6C8CDDC452CC541D1315A9C372F34F75695735D924B0E57F09AB17B19AE5C0E819ACD7A378AA24C96D7FF848C50FD9EDD50B135FF6B52CDC3CC9CA63EF61E49763A5A162479BC0798361BC21DC8CAB87B9379BDFDB8AEEBA4AEB2FD66B88E182643A93FE304BE7780DCAA736CBDD170C07988E27F03DBF05032CD6E8E59EA1CEE1895A4C800B42F7714", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 17`] = ` +{ + "ct": "000000000000000000000000000000000000000000000000000000000000000021D52C0956BED6AD4DD5DEB011CF11456B1F2749595EDC9E66FAB5CD8751389D175736A90CFCCFCA70AD6382D2C5EAE2064582840C78EC6EDF581F94CB826EE22CFD4CEFA358D0C089E1BC3A80F2C41F4964621ED2939AAE760A0FFEB9686FE929A7573797A02A8626C3A4DE747DF659955E5B94EDA52AAB25AE7FF8F5FD84F277B35F77EDE139643AD7C1FB35FB7333DD81C075370D88CEF88A892FE41F7C7D6451E5756A87A45FDC7B01E2B385364A1AA03687133B4F63AAD35E686CB4DD82A2378F05F4B6F118D507F6FA3EDC40E77128161C0613A832500ACD6A5B53AA7DE072CD4106D59CE683FBBDD07ACDEA664DD58607DF77CF57EDA5E02649DF607F91631CFC670843F73D5CED82ACA05571D05EEBA278E0EB69218E1C9BC072F1A86680B6ADC32019C13E4A2CF30088CDA1FBA10457B9896A1B929E4588B9F18C1E9F286F992E13D843047765628CDB91695EE892D6980B74F8D688D900D5F023AD0F3B4A50AD12E5960950EBD417C5085A2D9D67506D94D1A7F7E0A7A7C5431E26A0D7C4C5585524B1F1BDB5D36C67884B3352E0C0818A82F30C5D53AC6921FFB04A7C67EE31CC5D7BEC1A05BCFE1832AD5112362F470EC63A34ED38642086565485D7A03CD8F20EE402BDF33DA4EC43BE6C41D28FC8255C86ED4793B17A9335C57C59B5AA7A73510ECCC04F474196377E1AECBEF6369D8B374D29E68E368715D89843711B187C55B2290DFBA1E791FECF", + "key": "AA736CBDD170C07988E27F03DBF05032CD6E8E59EA1CEE1895A4C800B42F7714", + "pt": "0000000000000000000000000000000000000000000000000000000000000000C6E10E44CCA44E21CB02EDC1E7637F3D161CA1F0FADA875251E3FB56AD9AD7C219A512DF231C2B29247142802E3FA3DA8B007C134A6E32413AEECE2E526372E0A27FF21324DDD833CBCE5A7AE2F9D4E6A3DB6EF37D3AC248FEEFF015B82A41A48AE821E4059F5570EF54E5F793299BB6AB1058759CBA0EA2C9DC32E686C42CF3837E0A509E34C33C644E390A5DFEDB8E54616DC271377F68FE5F7F3C96C4A0A21985BE9E999C2BD649238C2BAF430D67AC234F598ECC2AF3C2BDBD7E5C3389A5E4FB173C139F95063E46A5063F704EEE6045A93A667D67ADF51AA7381C25AC2B16F5B757B20C8E03A4E0D0B3A26F457C79806EE171C9923E03163BDD12772D8D2B56B5D06D9D3DA241F80EE7CE1789BA275FCE916B8F9845EAD1E7CA9D488C99CAC05CBD8DC93A51253C694B533391CE380E4570976096CBBF5D9163B8F2482E581B1C404572D43A1608ACE64CDCB20C8644208A492AC9F56379FCA784B3DFA4E3654FC84505913D2A28F6FFF65693E0ECAB4A26B8435978FA99CBE68E639E5506539AF2A31B217EA1B48879A118FFFCD245A2DA17D4C8B952D63D56AA69317618B14585D9D0D427C69E1D3701954D79B21714122814255D32FA9C3A3FC960DD891B0387969852BBEBE93A836D3B0203FA3D17D366CD7B04905605106F70AB32F3638DD05C0FBBF0C9BA92069A5948F8", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 18`] = ` +{ + "ct": "000000000000000000000000000000000000000000000000000000000000000021D52C0956BED6AD4DD5DEB011CF11456B1F2749595EDC9E66FAB5CD8751389DF06314E496E65746F67A50F32469849A7B46043DAFFCB7A2E841510FE14981BD220F68998CB83423DD3D9D387C088D27C4219C899485448193BCDE44208973EBA725E9CB1025227564EC429E1676E6A07FE15779420C724DAD4B8013F4BFAABFD4FC29A47FDE4692F34080D2D2AF1EDCE3CFC3944612ACC714F8C4319726D47C909CB05219525E0782E2F913DB809EF793409B305501B8C5AC06A87B1E6F015DDFE3D4EE07AD7E91405F7B33221A7BCAC7AB6FC29BE4CDA238642E7C6BD4FE5AA6BE5578E1FCF8F868BAEE2C7B61E46F5CB83921BF1900C848B58A740EA9662967E466EAD3D151121A4780E17402FA6BE40B0344D65EB600CF3DC7609BDABC5ADCB51F81C9B5679442EECF96623F116A0CA02164AAE6193759C1BED9E4CBF12F3368858960FAFBD31F0120DADF60CD069D47D3F1B6E28828FB4B0DEBD4F3E79DC8083989C673E9EF1B2F2250D7C22B3FF531D50CBCB56CAA421182009400E22F4C89C15DB042501AD2C5A8F88DF413F1F264CDB6545D0A2C01243FED22017FC3ECF839D9CA8258B4BC1338163367451AB0057435D1508C706A66569EE3CE9892D71A825730EE87B82839EBB65B613C6A8F44F0B2A73FBFE1EB5037EF65DC034C7095161134190D51259486F9884176C38C907BAA9875ACB53038702F23648B2F177949613E00BF4C2C7726E03C5E8149CCF6F87BA6D684F5B656011FA23DF7D8AB9F72CFB228CC3643802B68E49B90CC", + "key": "AA736CBDD170C07988E27F03DBF05032CD6E8E59EA1CEE1895A4C800B42F7714", + "pt": "000000000000000000000000000000000000000000000000000000000000000021D52C0956BED6AD4DD5DEB011CF11456B1F2749595EDC9E66FAB5CD8751389D175736A90CFCCFCA70AD6382D2C5EAE2064582840C78EC6EDF581F94CB826EE22CFD4CEFA358D0C089E1BC3A80F2C41F4964621ED2939AAE760A0FFEB9686FE929A7573797A02A8626C3A4DE747DF659955E5B94EDA52AAB25AE7FF8F5FD84F277B35F77EDE139643AD7C1FB35FB7333DD81C075370D88CEF88A892FE41F7C7D6451E5756A87A45FDC7B01E2B385364A1AA03687133B4F63AAD35E686CB4DD82A2378F05F4B6F118D507F6FA3EDC40E77128161C0613A832500ACD6A5B53AA7DE072CD4106D59CE683FBBDD07ACDEA664DD58607DF77CF57EDA5E02649DF607F91631CFC670843F73D5CED82ACA05571D05EEBA278E0EB69218E1C9BC072F1A86680B6ADC32019C13E4A2CF30088CDA1FBA10457B9896A1B929E4588B9F18C1E9F286F992E13D843047765628CDB91695EE892D6980B74F8D688D900D5F023AD0F3B4A50AD12E5960950EBD417C5085A2D9D67506D94D1A7F7E0A7A7C5431E26A0D7C4C5585524B1F1BDB5D36C67884B3352E0C0818A82F30C5D53AC6921FFB04A7C67EE31CC5D7BEC1A05BCFE1832AD5112362F470EC63A34ED38642086565485D7A03CD8F20EE402BDF33DA4EC43BE6C41D28FC8255C86ED4793B17A9335C57C59B5AA7A73510ECCC04F474196377E1AECBEF6369D8B374D29E68E368715D89843711B187C55B2290DFBA1E791FECF", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 19`] = ` +{ + "ct": "0000000000000000000000000000000000000000000000000000000000000000F46D93C55C4C0B1FA2CAE7EC1E3A80B14F3951C2200911DA92DAABE373F9422E7DC05D46D7057EBA2E5ECF79EFBD6B2D872C41DA8D5A562AD61742C8ECEFB29B91C7FB5A618EA68F72D8E8C1AA4210EE2922A533E1D6C0D711A2501844A411D9E7F590125041BEFA1386F89D6BB1909E6BB7A2A6FA70EC1AD50B70A4F63ADF45C96EF6EC845C8AB45EF4DB173AA1E68EE774262B23FD2C1C2DE90B1D900C97477475194FAC8B35AFEC48EBF9E480C4E310AFA8282723A74436AE9F1841197A61D58A79047AC6CC7DA26677037FB539691BA66CEB6B2DA6F1682D8EA0EA40BA4F59BE8486E19E7C8308B863CA7F216C2E80175D3AD9DBCC276F9F3208FAF08F4BE41AD3FE41C79A553A8A26E1D1CFDB4E6A878EA9BA020024C969210AEF5A4D9E0BD158E9CDD40877BC62777F05562DC6A39E0AD05E5E8F32565A6B0B94E340AF7D1DDCB329E63DA0629B450E648DFF1FF300DA367B87623450BA534F2DA1E52BEA097C1F2C60B6436690A289967227D65F4835ACB44175F2B7EB1384AD73F8FD2B7B43774AB5647B109E46CB6C371D1244F1A59B3564942530E4D70E6FA80F9FEC8B6B2CE49DBB5082769C3965841C6E3488054071F78DDE10E4CBD461532AA3AE996D970C958674D682D308216B0DBD2AEBA62B99207BA034BC275B390E8B399CC31A77FCEB4D4A80CF7599AB453C5BFF1D3077EAD03EE13BF2D64B20B7F658115CCE606D7F8DE9213DF44F4361A4A037BB69A15037C6429C00342EC5EF47B8E5D8467655ECC1E4A4BAE068B9357942528464C4245FF6824BD8626FEE3DE23AB854B82D2BDC512674475F37B790600E", + "key": "1AECBEF6369D8B374D29E68E368715D89843711B187C55B2290DFBA1E791FECF", + "pt": "000000000000000000000000000000000000000000000000000000000000000021D52C0956BED6AD4DD5DEB011CF11456B1F2749595EDC9E66FAB5CD8751389DF06314E496E65746F67A50F32469849A7B46043DAFFCB7A2E841510FE14981BD220F68998CB83423DD3D9D387C088D27C4219C899485448193BCDE44208973EBA725E9CB1025227564EC429E1676E6A07FE15779420C724DAD4B8013F4BFAABFD4FC29A47FDE4692F34080D2D2AF1EDCE3CFC3944612ACC714F8C4319726D47C909CB05219525E0782E2F913DB809EF793409B305501B8C5AC06A87B1E6F015DDFE3D4EE07AD7E91405F7B33221A7BCAC7AB6FC29BE4CDA238642E7C6BD4FE5AA6BE5578E1FCF8F868BAEE2C7B61E46F5CB83921BF1900C848B58A740EA9662967E466EAD3D151121A4780E17402FA6BE40B0344D65EB600CF3DC7609BDABC5ADCB51F81C9B5679442EECF96623F116A0CA02164AAE6193759C1BED9E4CBF12F3368858960FAFBD31F0120DADF60CD069D47D3F1B6E28828FB4B0DEBD4F3E79DC8083989C673E9EF1B2F2250D7C22B3FF531D50CBCB56CAA421182009400E22F4C89C15DB042501AD2C5A8F88DF413F1F264CDB6545D0A2C01243FED22017FC3ECF839D9CA8258B4BC1338163367451AB0057435D1508C706A66569EE3CE9892D71A825730EE87B82839EBB65B613C6A8F44F0B2A73FBFE1EB5037EF65DC034C7095161134190D51259486F9884176C38C907BAA9875ACB53038702F23648B2F177949613E00BF4C2C7726E03C5E8149CCF6F87BA6D684F5B656011FA23DF7D8AB9F72CFB228CC3643802B68E49B90CC", +} +`; + +exports[`AES > Cipher must match reference test vectors cascade 20`] = ` +{ + "ct": "0000000000000000000000000000000000000000000000000000000000000000F46D93C55C4C0B1FA2CAE7EC1E3A80B14F3951C2200911DA92DAABE373F9422EA878E28ADDF7A308C141F625E048FAD9A30A3751F40D9B6E22375CE61847C8281C64B2F8206D8F73AAFC774B6196FF59D548E0D4C370215F2FF443DF490222FF543D03D1BD772C56BC638D64BDFB0D5786B49B1C8F23684C5715FEF89217BD7789BE8F35C438163B299E6114476690B0F322D3F49B81B24B55A9FBAA9289E2BD69E7C6075709F98941FCB03C0C8E3CB114144D9742CC279F0FBF50344633395A3163D019CF1FA7D5CCCC65E940B5637D98495FF3190FB970F285B9C3B536C17353D7296C9CF5CE6FEA816FFA228E2E8D5C1A5E132912A7743FD692D47B64CB5E1B1A020041A51E2E5A88AB07D58F530FB628EAB2DCC0CCCBEE4399761B03A4FC882FEDFD5FC2C3309CAFD17FA09B0CE32D12873D32023916500E8D61E063B16BAA799BDB2D8752439C17FDE703E4C3B35C3EF1828F3FF4315F21869D5D8954ABA47C2525657C70301B0AC75D2D9F15CF310F3C6B79249FEE1C1A4D205421FA4B097A06E1A0A63BD76D21C6122D8711FBEE88453B3D908D7DC51E468A56DB154D8B79E9061E6A8F31402D720A8447128D821D6D6D10CE13D7212423372CFA5AFFAEEA3F62228A6590E8E77727778854C9AE66D75E39877A0E4E3EBA11BB933908E540F5B7C0904C867E744D27D14F0D8C5AB266EED4CFFAA0E41EC6FF7C657E2DFD0AC206A58DCDF28466072F6065EE384436227C2292541697CA924AC63C3ACFE3FDC1770693F341A9F032C7C60A5CABA9C9F51ED2BEB435618E575E89EF525AF6138C94CC185CF4937D9437EA3E898098A19E42AC3133C94133CE723C8ED9A901B3C5AE4DC58A3C010BE138F11AB2F9", + "key": "1AECBEF6369D8B374D29E68E368715D89843711B187C55B2290DFBA1E791FECF", + "pt": "0000000000000000000000000000000000000000000000000000000000000000F46D93C55C4C0B1FA2CAE7EC1E3A80B14F3951C2200911DA92DAABE373F9422E7DC05D46D7057EBA2E5ECF79EFBD6B2D872C41DA8D5A562AD61742C8ECEFB29B91C7FB5A618EA68F72D8E8C1AA4210EE2922A533E1D6C0D711A2501844A411D9E7F590125041BEFA1386F89D6BB1909E6BB7A2A6FA70EC1AD50B70A4F63ADF45C96EF6EC845C8AB45EF4DB173AA1E68EE774262B23FD2C1C2DE90B1D900C97477475194FAC8B35AFEC48EBF9E480C4E310AFA8282723A74436AE9F1841197A61D58A79047AC6CC7DA26677037FB539691BA66CEB6B2DA6F1682D8EA0EA40BA4F59BE8486E19E7C8308B863CA7F216C2E80175D3AD9DBCC276F9F3208FAF08F4BE41AD3FE41C79A553A8A26E1D1CFDB4E6A878EA9BA020024C969210AEF5A4D9E0BD158E9CDD40877BC62777F05562DC6A39E0AD05E5E8F32565A6B0B94E340AF7D1DDCB329E63DA0629B450E648DFF1FF300DA367B87623450BA534F2DA1E52BEA097C1F2C60B6436690A289967227D65F4835ACB44175F2B7EB1384AD73F8FD2B7B43774AB5647B109E46CB6C371D1244F1A59B3564942530E4D70E6FA80F9FEC8B6B2CE49DBB5082769C3965841C6E3488054071F78DDE10E4CBD461532AA3AE996D970C958674D682D308216B0DBD2AEBA62B99207BA034BC275B390E8B399CC31A77FCEB4D4A80CF7599AB453C5BFF1D3077EAD03EE13BF2D64B20B7F658115CCE606D7F8DE9213DF44F4361A4A037BB69A15037C6429C00342EC5EF47B8E5D8467655ECC1E4A4BAE068B9357942528464C4245FF6824BD8626FEE3DE23AB854B82D2BDC512674475F37B790600E", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 1`] = ` +{ + "ct": "00000000000000000000000000000000008910494504181950F98DD998A82B6749", + "key": "0000000000000000000000000000000000000000000000000000000000000000", + "pt": "00000000000000000000000000000000", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 2`] = ` +{ + "ct": "0F000000000000000000000000000000008910494504181950F98DD998A82B67499F230E3B7EE772B153CB91FA1B76ADCA433E7E267FBEF117CE63FCB3F0092CBC", + "key": "0000000000000000000000000000000000000000000000000000000000000000", + "pt": "00000000000000000000000000000000008910494504181950F98DD998A82B6749", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 3`] = ` +{ + "ct": "0F00000000000000000000000000000000CA5AD3D7F1D384BF1760D0E9448B7E0CB94697A01AF8F028545BB2621AC0A6B01467D7BA5ED22CC220C25D7ABC4C333BEECB2FEFAA8A59CDBC7344A96EBCE5CC0307B7E750D5296F5AF02415FF78B2B8", + "key": "000000000000000000000000000000008910494504181950F98DD998A82B6749", + "pt": "0F000000000000000000000000000000008910494504181950F98DD998A82B67499F230E3B7EE772B153CB91FA1B76ADCA433E7E267FBEF117CE63FCB3F0092CBC", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 4`] = ` +{ + "ct": "0F00000000000000000000000000000000CA5AD3D7F1D384BF1760D0E9448B7E0CB905DD3A880D3BB5BBB55F6B6B2C06A95141B223C5B63340B9C5CD59244D8530949C764636AB3510699DE508A7F0A0D384E97CC8BF7FA336974C575156160E5D45318ACF5B63069CE42A43AF0B5EDA8556F6E3F71F3DFA5B6F51BB6C6F4DCD85", + "key": "000000000000000000000000000000008910494504181950F98DD998A82B6749", + "pt": "0F00000000000000000000000000000000CA5AD3D7F1D384BF1760D0E9448B7E0CB94697A01AF8F028545BB2621AC0A6B01467D7BA5ED22CC220C25D7ABC4C333BEECB2FEFAA8A59CDBC7344A96EBCE5CC0307B7E750D5296F5AF02415FF78B2B8", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 5`] = ` +{ + "ct": "0F000000000000000000000000000000000E2328B0E889F546B1262ACAE382F0E3CCAC74535BEEE45DA7414E83C60926ADFDDA9856FED57CE70893B8D123D54DA38FF7834824C37AD9299B21D16EDB73573F959A46CEB4431DD9F176566DEF9F3F8DB1B42D88DC9DF12CF152B7A98C8A034DF4AD0742F12E22E28ADB86D2D48915E3E5B034AFFB1F4ED3AFF4B98A4253C5E00F2F1E0106B06430C2DFA96889ADC0", + "key": "EECB2FEFAA8A59CDBC7344A96EBCE5CC0307B7E750D5296F5AF02415FF78B2B8", + "pt": "0F00000000000000000000000000000000CA5AD3D7F1D384BF1760D0E9448B7E0CB905DD3A880D3BB5BBB55F6B6B2C06A95141B223C5B63340B9C5CD59244D8530949C764636AB3510699DE508A7F0A0D384E97CC8BF7FA336974C575156160E5D45318ACF5B63069CE42A43AF0B5EDA8556F6E3F71F3DFA5B6F51BB6C6F4DCD85", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 6`] = ` +{ + "ct": "0F000000000000000000000000000000000E2328B0E889F546B1262ACAE382F0E3CC680DA83CF7BE2C5EE70879E5AE2F2312AF31FF97069F38E08F4CC0CB7868838B5B186251F819968E2A77A4E6DCEB9FAC8EF1B3C0A62B5210B17092B426B4EC090AC8CB06AD561107BFEF96AEB773922F3C2D39A02291B98F420097CA765B4565FEB27A5FA6D39AAA222FD960FFCA8170ECCAAE35A94B7B7E11705DD103EF9384778998B96BA3CA0A8E6E3B8AA463026DFF3BC5CE1FEFA6376D2BDF7D8A2849", + "key": "EECB2FEFAA8A59CDBC7344A96EBCE5CC0307B7E750D5296F5AF02415FF78B2B8", + "pt": "0F000000000000000000000000000000000E2328B0E889F546B1262ACAE382F0E3CCAC74535BEEE45DA7414E83C60926ADFDDA9856FED57CE70893B8D123D54DA38FF7834824C37AD9299B21D16EDB73573F959A46CEB4431DD9F176566DEF9F3F8DB1B42D88DC9DF12CF152B7A98C8A034DF4AD0742F12E22E28ADB86D2D48915E3E5B034AFFB1F4ED3AFF4B98A4253C5E00F2F1E0106B06430C2DFA96889ADC0", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 7`] = ` +{ + "ct": "0F00000000000000000000000000000000768F6371470CCDC5C7D0200BC4E30257A3C5535DB7978A4F80ECA575D622D055B4E62100A9AADCAAE9DADB123302527BC85CDC0361F56B5DE59655EBB780C1327D052CBAF2699F8065A622A6063ABB07CACC149FC8FBDCAC86CE30E814E591CEF34AACD815908ED1C2842997CB9812F9E7F29FF5BAB50D6FBF306DC516383D80070D1F86BC31A547094DBE6D66F42DCD20FAC32AD82BC11E143C15955C3CA479BCA21F602CA9528A7DA20698EEF5FAE8A57FAB9FFFEE58975E638B90D1AD36CCD95F1763C666C95F5E643EDEC13365AA", + "key": "E3E5B034AFFB1F4ED3AFF4B98A4253C5E00F2F1E0106B06430C2DFA96889ADC0", + "pt": "0F000000000000000000000000000000000E2328B0E889F546B1262ACAE382F0E3CC680DA83CF7BE2C5EE70879E5AE2F2312AF31FF97069F38E08F4CC0CB7868838B5B186251F819968E2A77A4E6DCEB9FAC8EF1B3C0A62B5210B17092B426B4EC090AC8CB06AD561107BFEF96AEB773922F3C2D39A02291B98F420097CA765B4565FEB27A5FA6D39AAA222FD960FFCA8170ECCAAE35A94B7B7E11705DD103EF9384778998B96BA3CA0A8E6E3B8AA463026DFF3BC5CE1FEFA6376D2BDF7D8A2849", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 8`] = ` +{ + "ct": "0F00000000000000000000000000000000768F6371470CCDC5C7D0200BC4E30257A3BDFF1676380F77039A537F1705B1A700898C5E5C21BC9E8A04D0BF3F31DE84BEFA95139ECBC71E779F007C6578BB0885462B7E935992F2AECD9E84496BE72D671D9F42C1C9131854BB27BA20578DC118896A04415ED85B7F055848B522401BBB2EE9745B00BF70D77DABEC1639D3C9BB8F13AB33D4B699FC58AC2F7A82EA3A218D22FFF0A259F0284B495B6C8B53BBE206922A9EC812E8A9BCB4E34023622FDEAEF6BB5A0CEE2A722944BD963E491E78FA68C859992707C93A5D5551E2C89CE59644FD3311CBE757F4BA3972FCCF6DFA600F79A784E37C1938A67D5833749E", + "key": "E3E5B034AFFB1F4ED3AFF4B98A4253C5E00F2F1E0106B06430C2DFA96889ADC0", + "pt": "0F00000000000000000000000000000000768F6371470CCDC5C7D0200BC4E30257A3C5535DB7978A4F80ECA575D622D055B4E62100A9AADCAAE9DADB123302527BC85CDC0361F56B5DE59655EBB780C1327D052CBAF2699F8065A622A6063ABB07CACC149FC8FBDCAC86CE30E814E591CEF34AACD815908ED1C2842997CB9812F9E7F29FF5BAB50D6FBF306DC516383D80070D1F86BC31A547094DBE6D66F42DCD20FAC32AD82BC11E143C15955C3CA479BCA21F602CA9528A7DA20698EEF5FAE8A57FAB9FFFEE58975E638B90D1AD36CCD95F1763C666C95F5E643EDEC13365AA", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 9`] = ` +{ + "ct": "0F000000000000000000000000000000008F3377D44E9B664B4FDDADAB47BE6A552A00F06FB240251C95B44C0736FBB4144879FA83B7C864D67EACAA8188A1144D01DFE75A564CCF66BAB7D2E2A7B0FED749F1CCC46D583B2D551ABDB01822DD25C5D1C76739DA84E8E259DA8D10C052CDEF5CEC1D414BBD6BAEDFCBEF4B1A561810CA1E4B7934175299EAF9074728DA862A98D7ADB9C0BE69C2BF491460AA6215FB316E4B71BBE3E52396B0261909A7B1D5003AEA6D9C1BFA87DDF2CB747CCF0B7B24E4B384CC37DDED3079A1E1253CBBA59E0A8A880C775A55C4D1B7F0550B6875ABFE48C69BD66D290405E027FB1354AAF2FEF9B64D7608409BFFEED831F54BEE70B38A83A321C265666E37E629EBADA8F857E2632FED7B73A749741EF5330F", + "key": "A57FAB9FFFEE58975E638B90D1AD36CCD95F1763C666C95F5E643EDEC13365AA", + "pt": "0F00000000000000000000000000000000768F6371470CCDC5C7D0200BC4E30257A3BDFF1676380F77039A537F1705B1A700898C5E5C21BC9E8A04D0BF3F31DE84BEFA95139ECBC71E779F007C6578BB0885462B7E935992F2AECD9E84496BE72D671D9F42C1C9131854BB27BA20578DC118896A04415ED85B7F055848B522401BBB2EE9745B00BF70D77DABEC1639D3C9BB8F13AB33D4B699FC58AC2F7A82EA3A218D22FFF0A259F0284B495B6C8B53BBE206922A9EC812E8A9BCB4E34023622FDEAEF6BB5A0CEE2A722944BD963E491E78FA68C859992707C93A5D5551E2C89CE59644FD3311CBE757F4BA3972FCCF6DFA600F79A784E37C1938A67D5833749E", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 10`] = ` +{ + "ct": "0F000000000000000000000000000000008F3377D44E9B664B4FDDADAB47BE6A552AF94C7B1749B2B71B3C418A9678E97C4AF0478CCE0C1CFC153A849EF080EA48B297172C8BA726BEF2437A9899076E1D804EE9B62490BC252DD7956286E015601A1D7080832485413DA20DAE24911BF7E7FE204564B3AEFC5E6929127C2AC1C71C3DCBCD60340237A93B2394E0D6E2902933335A86E28AC1E0F1DE468BFB731CB4A0798F7731F7EDD3A857C322138F39FADA86A6D91D024092D62F3209094DFF7113E21B443F63D4FF1E18E7C9116316813B809880D2B783A25BC88AEC22101DD0769A2A844A433D7498FB6CC55AA4975E62C34303B8FC15CAE50F510164F297D720217B03B2E857113FCD6E75A9E92C7D162751E9AC4E5AB1C22F1A29131AE4CE503CE2031DFBF9DD2C76342367E2404363AC938B980B575C4BC43F48387524", + "key": "A57FAB9FFFEE58975E638B90D1AD36CCD95F1763C666C95F5E643EDEC13365AA", + "pt": "0F000000000000000000000000000000008F3377D44E9B664B4FDDADAB47BE6A552A00F06FB240251C95B44C0736FBB4144879FA83B7C864D67EACAA8188A1144D01DFE75A564CCF66BAB7D2E2A7B0FED749F1CCC46D583B2D551ABDB01822DD25C5D1C76739DA84E8E259DA8D10C052CDEF5CEC1D414BBD6BAEDFCBEF4B1A561810CA1E4B7934175299EAF9074728DA862A98D7ADB9C0BE69C2BF491460AA6215FB316E4B71BBE3E52396B0261909A7B1D5003AEA6D9C1BFA87DDF2CB747CCF0B7B24E4B384CC37DDED3079A1E1253CBBA59E0A8A880C775A55C4D1B7F0550B6875ABFE48C69BD66D290405E027FB1354AAF2FEF9B64D7608409BFFEED831F54BEE70B38A83A321C265666E37E629EBADA8F857E2632FED7B73A749741EF5330F", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 11`] = ` +{ + "ct": "0F00000000000000000000000000000000FDE5E37024E7F45DFC02091CD12453F4DF89E9E0BD239670C4E43E59A1D7F0DE62B0D9801435BA10E9146B45C160ED3573E548F714AE0E9F16B3882E17050D6DF418A680E6C78A1528623767993C8A279C6B1DC58223C3017ED970B4F3158598C0C861EB5FB3AECB96FF7ADE2194D990B41CC36194208718B1E6A1F7732BD6DA4DD7144D63DB37E0474DF88667DAFA28288F585B485127EAA7AEBDD5BA5D2FF40BC0B18526A164F5E0F17D3926A3B1631F78CAD906B195D62CBF7BD63FBF94236A996EA0CBE9C756D1B40056239ECBB889E48E5A138BCA4DC39C9E240F5145446BA0CF7E6919400F3A1D19006823F77D641A28B400B0F5DD4AD1FEE7C22016C5D50B2AA1290785CB69B83D2EB20D5CE109D2BF8B65F7EA803348EE6267D482266ACA649D4D7A6112B037879853143D939F90D5A166319B16B4AAAA78DD7AE35E55E18CA90A1FAA83688ABDB3B32D29BB", + "key": "EE70B38A83A321C265666E37E629EBADA8F857E2632FED7B73A749741EF5330F", + "pt": "0F000000000000000000000000000000008F3377D44E9B664B4FDDADAB47BE6A552AF94C7B1749B2B71B3C418A9678E97C4AF0478CCE0C1CFC153A849EF080EA48B297172C8BA726BEF2437A9899076E1D804EE9B62490BC252DD7956286E015601A1D7080832485413DA20DAE24911BF7E7FE204564B3AEFC5E6929127C2AC1C71C3DCBCD60340237A93B2394E0D6E2902933335A86E28AC1E0F1DE468BFB731CB4A0798F7731F7EDD3A857C322138F39FADA86A6D91D024092D62F3209094DFF7113E21B443F63D4FF1E18E7C9116316813B809880D2B783A25BC88AEC22101DD0769A2A844A433D7498FB6CC55AA4975E62C34303B8FC15CAE50F510164F297D720217B03B2E857113FCD6E75A9E92C7D162751E9AC4E5AB1C22F1A29131AE4CE503CE2031DFBF9DD2C76342367E2404363AC938B980B575C4BC43F48387524", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 12`] = ` +{ + "ct": "0F00000000000000000000000000000000FDE5E37024E7F45DFC02091CD12453F4DFFB3F741949EAE2D257E1FD16416AE7C345A9258F9FD0342ECBB33A1257422CD1CD086918743739FA4FA6C1CC34ED6A89D9D4DF3D58833D0986C7952FB28844EC1F4B8AB4E194374EDCC516F60A5907874E17861AB2A98DD6BC01A33B435D0EDB3BF5203A1B8718862E37A4BF7668C21A7F3545CF2F2365685525040449071C62EBBC7C5FB41E57860901F37AB10E7D3F5C9EA4F29E0425E7857BD3303BFFC3D289D0EE254E29B099CD5C8434903EDFF6F7058809AB49A0D367A1351268654FBC0F2CB42BC0F13D16EF71ECD39EF99FCEF95D6A198E81864AAA1D6520E9FC9CB72FEAB83DDA5461502106F1934951C03FB810A8E60487D6E3E3D31D3BBAD51EE07AA2869537414BA2909470534F9C606F0DE61E241C8B03C9D9E30005508EF3F9B97C6968F7797CF146D63B7A61CF16E27E1C7CAB799B187E3E1719CBF05358EC084636F71F473EEB71AF4B3BA6C78037C9B91A8B0A14CD9E9374053FBA2A46", + "key": "EE70B38A83A321C265666E37E629EBADA8F857E2632FED7B73A749741EF5330F", + "pt": "0F00000000000000000000000000000000FDE5E37024E7F45DFC02091CD12453F4DF89E9E0BD239670C4E43E59A1D7F0DE62B0D9801435BA10E9146B45C160ED3573E548F714AE0E9F16B3882E17050D6DF418A680E6C78A1528623767993C8A279C6B1DC58223C3017ED970B4F3158598C0C861EB5FB3AECB96FF7ADE2194D990B41CC36194208718B1E6A1F7732BD6DA4DD7144D63DB37E0474DF88667DAFA28288F585B485127EAA7AEBDD5BA5D2FF40BC0B18526A164F5E0F17D3926A3B1631F78CAD906B195D62CBF7BD63FBF94236A996EA0CBE9C756D1B40056239ECBB889E48E5A138BCA4DC39C9E240F5145446BA0CF7E6919400F3A1D19006823F77D641A28B400B0F5DD4AD1FEE7C22016C5D50B2AA1290785CB69B83D2EB20D5CE109D2BF8B65F7EA803348EE6267D482266ACA649D4D7A6112B037879853143D939F90D5A166319B16B4AAAA78DD7AE35E55E18CA90A1FAA83688ABDB3B32D29BB", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 13`] = ` +{ + "ct": "0F000000000000000000000000000000006D0D3031A1518F43DC73ADC761D535356AF27C9A03B6663F1CE8D9E50C895704448FF1015D668495E965B9ECED473C24FE77DD9615D4ECC141F39C50C8C9CA6F9A1FAEE1C432AC30AE8679CA9CD765D6FDC80B3DDDD4FC69C5477B10E1F345125E9FA934D985AD38624DABBAD1CFA17BDA001A74563F9ACF523A29DCCF835532C7B5CC7FDE0465D2A098081BB2FEC0885007027E83D34BA0616D9DBDB61E0DAA7577CB0908EE2ADD1C4D59C2919B3D9CD32312FEE26EB4EC6D427B3F49AECC84810035BF4F3F57C1EF5BEDDFD9135DC19E90817FDFC8EE19F4BCB76F9A34CC950B1F92CF3BF782445C762957929500AE4E70379B40D06EF73404A3BD0CC47C2317B5F3799AE8CE5E63AFD51CD49A02CF706038CC28D6D1C3D2BCC2FDA7EB1152B957460A01DFC9C0FC85D51C8735C660690C1763D2B3F377F338DD6F2BEC42E7AB3CE6E53848BF13A622B5E06F22762D1708E542F0D1D095415092B6295B029A8B40A7031D8B27F8AB3A947F4C9AB014680D4D532FC4B1B0D61934ED9B784CC2467B1E9F1011B9EB600B3CCC742639E2", + "key": "9F90D5A166319B16B4AAAA78DD7AE35E55E18CA90A1FAA83688ABDB3B32D29BB", + "pt": "0F00000000000000000000000000000000FDE5E37024E7F45DFC02091CD12453F4DFFB3F741949EAE2D257E1FD16416AE7C345A9258F9FD0342ECBB33A1257422CD1CD086918743739FA4FA6C1CC34ED6A89D9D4DF3D58833D0986C7952FB28844EC1F4B8AB4E194374EDCC516F60A5907874E17861AB2A98DD6BC01A33B435D0EDB3BF5203A1B8718862E37A4BF7668C21A7F3545CF2F2365685525040449071C62EBBC7C5FB41E57860901F37AB10E7D3F5C9EA4F29E0425E7857BD3303BFFC3D289D0EE254E29B099CD5C8434903EDFF6F7058809AB49A0D367A1351268654FBC0F2CB42BC0F13D16EF71ECD39EF99FCEF95D6A198E81864AAA1D6520E9FC9CB72FEAB83DDA5461502106F1934951C03FB810A8E60487D6E3E3D31D3BBAD51EE07AA2869537414BA2909470534F9C606F0DE61E241C8B03C9D9E30005508EF3F9B97C6968F7797CF146D63B7A61CF16E27E1C7CAB799B187E3E1719CBF05358EC084636F71F473EEB71AF4B3BA6C78037C9B91A8B0A14CD9E9374053FBA2A46", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 14`] = ` +{ + "ct": "0F000000000000000000000000000000006D0D3031A1518F43DC73ADC761D535356A6294494233D04402C8A841D739A662853AF842B37C7B1934AB06D4F55DF4191DF017CE31061595E034325A1E36DA11923014343B3F0CEB563DC5F00DD398F1F8DBCD47E32D9646C8E07BAEBE4020FFCC8E7E746EEC98503CC63004D7D85867CFD9CBCAE4FCADCBE78ED876D669D9CEB2B4F7908A6841CF774C1C05CA8E35B5A0DAC887B9C260E6D6A55090A9A8BA6DE14527B70A324D88EBAA3D5EDF57929F046939AB4F94C4C295B9B31D580F6C46DE019F7D5FF8775CB3AF62F8626E6333C5E7764FE88E7A0795808B2370FFB7AD853D0D62F0038A5B78947A9111DCAA9B44B5D154E5F217F4F6127F893E7600DF254CACA4B995C464F5CBF0B998058FE29348352FF9AA3D8A5A3C8EFBA604318568C75C904B62285074F5F94A0AC162ED5BDA4DC3C6963035300D8159376E27AF38AC538E32F2FB99AD20CBEB3B73FBA0E641A7B86942E1B14A888E14D0FFD0BFFEBBA7A0698CE96F0090B542B1884DD172B1C44D36523083E32C9D0DE10B6C58141313D2433E7D5AD0DD25F899BD41AE6E3ED09689EA0D3F3B99A87604924C475910BC42D3E02DCF017DEF82A04AA85E", + "key": "9F90D5A166319B16B4AAAA78DD7AE35E55E18CA90A1FAA83688ABDB3B32D29BB", + "pt": "0F000000000000000000000000000000006D0D3031A1518F43DC73ADC761D535356AF27C9A03B6663F1CE8D9E50C895704448FF1015D668495E965B9ECED473C24FE77DD9615D4ECC141F39C50C8C9CA6F9A1FAEE1C432AC30AE8679CA9CD765D6FDC80B3DDDD4FC69C5477B10E1F345125E9FA934D985AD38624DABBAD1CFA17BDA001A74563F9ACF523A29DCCF835532C7B5CC7FDE0465D2A098081BB2FEC0885007027E83D34BA0616D9DBDB61E0DAA7577CB0908EE2ADD1C4D59C2919B3D9CD32312FEE26EB4EC6D427B3F49AECC84810035BF4F3F57C1EF5BEDDFD9135DC19E90817FDFC8EE19F4BCB76F9A34CC950B1F92CF3BF782445C762957929500AE4E70379B40D06EF73404A3BD0CC47C2317B5F3799AE8CE5E63AFD51CD49A02CF706038CC28D6D1C3D2BCC2FDA7EB1152B957460A01DFC9C0FC85D51C8735C660690C1763D2B3F377F338DD6F2BEC42E7AB3CE6E53848BF13A622B5E06F22762D1708E542F0D1D095415092B6295B029A8B40A7031D8B27F8AB3A947F4C9AB014680D4D532FC4B1B0D61934ED9B784CC2467B1E9F1011B9EB600B3CCC742639E2", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 15`] = ` +{ + "ct": "0F00000000000000000000000000000000EA4129D52CE10A617173FCD6385EF92E6B12E5FF54C423C18A0162ED32CA594E5D5EA2B0FD20C07B7BC9807177E127B541FEAA0DBF613FD1AF0119C78A4B91AC792CBECAFB5ADF5480FD8E56155631FBEA8D7704EFAA41162CB5E77C746304C7254E4B818539F19821CD0AD641937B5FAA6E082D8527812B4FCBAFC3A76067C701E2E1FB63009CD128BF2C146925CC767A330D678625D79D4160FFF67F68588FF2E8D1970D6A20B6804FA00D925B5D0B3F741147A63074D0D9CD476066B0C11BE44F1EE5DCDCEF5C1EB12D002DB3AEE482EEC0852B8508B37B0B612E9C779E0C47B01D557F1DFA0D09E2F9FA4514F012B7260872052B10ED3F05F41B3BC81B3C95CDFA5F2341FDD07697268A90E940064F6E356E812881A023CA5CF244E5E9EBACAE1E8C90872105ECDE6963E4D59AA9960CAAF0E269C5195A5E82436B871144C697816D84CA6E59E029297074FB83312E542983D27811C2C3600E95FFDF9D86A44C18216B7FA63578D3A241E32F434AB8C856FD69256570C9D76E34D5A5190B50F3B6201A04F8DA57092CC84B363CD9AF1260E6639B5B0D22242FE8DD2AAF94AAA80452EDE1C63B41F28E92837CEA2ED9C3C43B84F2CA1A97BF1AE429C161EDD233518B2A3DC1E2004AAD91F8B1B876", + "key": "680D4D532FC4B1B0D61934ED9B784CC2467B1E9F1011B9EB600B3CCC742639E2", + "pt": "0F000000000000000000000000000000006D0D3031A1518F43DC73ADC761D535356A6294494233D04402C8A841D739A662853AF842B37C7B1934AB06D4F55DF4191DF017CE31061595E034325A1E36DA11923014343B3F0CEB563DC5F00DD398F1F8DBCD47E32D9646C8E07BAEBE4020FFCC8E7E746EEC98503CC63004D7D85867CFD9CBCAE4FCADCBE78ED876D669D9CEB2B4F7908A6841CF774C1C05CA8E35B5A0DAC887B9C260E6D6A55090A9A8BA6DE14527B70A324D88EBAA3D5EDF57929F046939AB4F94C4C295B9B31D580F6C46DE019F7D5FF8775CB3AF62F8626E6333C5E7764FE88E7A0795808B2370FFB7AD853D0D62F0038A5B78947A9111DCAA9B44B5D154E5F217F4F6127F893E7600DF254CACA4B995C464F5CBF0B998058FE29348352FF9AA3D8A5A3C8EFBA604318568C75C904B62285074F5F94A0AC162ED5BDA4DC3C6963035300D8159376E27AF38AC538E32F2FB99AD20CBEB3B73FBA0E641A7B86942E1B14A888E14D0FFD0BFFEBBA7A0698CE96F0090B542B1884DD172B1C44D36523083E32C9D0DE10B6C58141313D2433E7D5AD0DD25F899BD41AE6E3ED09689EA0D3F3B99A87604924C475910BC42D3E02DCF017DEF82A04AA85E", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 16`] = ` +{ + "ct": "0F00000000000000000000000000000000EA4129D52CE10A617173FCD6385EF92E6B95A9E6B0499344A8AC62BC2393D282465FD2C14B363788FE4149BBDB04D44A6D26CE574D2F636ACD4E7B412FC92D7FD570B07738D4B87EC4B2BB7D88C24CB057666BAE116A24C593632737D27B816E2F5C1D3BC635764F71295F4A9359587B9287C81870CC544287D6A4F975F62CE439875638846147FDC8176963DC54C5C873805B71EDCCBF405F3F0CC66ECBF37631323852ED55C701FBD865A2F48D9DE9DD67BCB186372CBDE7A6A2FD35FDCDD4707403CD30354BEC0CFD59F4508D1149DFD48E04B3062C2B7BA67F61643843C190F714E3B5DEF17FBD0C721048F8783B16E4856232A40E9D69748298509CD3661C3E698605A124D76F5E310102ECFE1DACDEB4387AB255999749002477ED05244872388CD1FFA3B9C6A79FB1ED377B71F8C8C3B2FEB220100FC6A9D3426905BC825A578AB7EE91ACCC437A736EA76A07C5AA125131CE2957032D0777649015FE35840DAF50C49CC50B5A4AC1620063078192A142E827963F93AF2D23D6F7BE05CB39CFB2AA5B8F8FA423D73B720292ACFC56804391C26188A2A3FBE1EDF824E9DD6928E29D0BB76D73EB33151DA552CD0A307C8394CCCBF163FF958539E257AFA2EA924F11B933281ADD128B1C987917FD44EEA4BD57F4327A2561FD75B91F522A9EAEB93FA87B0BB304C8FA4380DF66", + "key": "680D4D532FC4B1B0D61934ED9B784CC2467B1E9F1011B9EB600B3CCC742639E2", + "pt": "0F00000000000000000000000000000000EA4129D52CE10A617173FCD6385EF92E6B12E5FF54C423C18A0162ED32CA594E5D5EA2B0FD20C07B7BC9807177E127B541FEAA0DBF613FD1AF0119C78A4B91AC792CBECAFB5ADF5480FD8E56155631FBEA8D7704EFAA41162CB5E77C746304C7254E4B818539F19821CD0AD641937B5FAA6E082D8527812B4FCBAFC3A76067C701E2E1FB63009CD128BF2C146925CC767A330D678625D79D4160FFF67F68588FF2E8D1970D6A20B6804FA00D925B5D0B3F741147A63074D0D9CD476066B0C11BE44F1EE5DCDCEF5C1EB12D002DB3AEE482EEC0852B8508B37B0B612E9C779E0C47B01D557F1DFA0D09E2F9FA4514F012B7260872052B10ED3F05F41B3BC81B3C95CDFA5F2341FDD07697268A90E940064F6E356E812881A023CA5CF244E5E9EBACAE1E8C90872105ECDE6963E4D59AA9960CAAF0E269C5195A5E82436B871144C697816D84CA6E59E029297074FB83312E542983D27811C2C3600E95FFDF9D86A44C18216B7FA63578D3A241E32F434AB8C856FD69256570C9D76E34D5A5190B50F3B6201A04F8DA57092CC84B363CD9AF1260E6639B5B0D22242FE8DD2AAF94AAA80452EDE1C63B41F28E92837CEA2ED9C3C43B84F2CA1A97BF1AE429C161EDD233518B2A3DC1E2004AAD91F8B1B876", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 17`] = ` +{ + "ct": "0F0000000000000000000000000000000029D6C4FB2524D316DB175938B64F3A772C45309DBC83E6178800E8816796702A2E6D96C7819F43A55BDCD010A748DE0FB784D0454622F95782EFD7850F8EEE1B977A526F02ABF7C900F3C39781263C8A8557D202D2CFD5F7A24FAC05500D2BA532A09D08CEC6870F9EF1F2ACB3657D810A3F61F15ADBF0A62761BC7775AF9F021DB7A8BA0378A133D61E0B81207D09A5F41EAC148E1CC54C4C1C97E972132D305DF7138F88808710377F5FB846049619972745EAD83A5613813F2F65AC401589F17A5D9FAC7488542464A42CA8FFFCE11CFBF441022A31E0647D0B1B006D689C264FB9D9BF7C7C543D1921FD3301ABBC0B99A6844C10658E9ECFA9FE338CF619C483952686124EDEA7DDE95F11C0495EE52DE6E4BB73C34358FA586FBCDA85CA299278ABDEE785F694E4F61B8BFB028720C10378C8519D4F7F40653252CA23EA4D7E41BF35C6A1D627C3980F5DAB17FC67C14311741F607A146BB3F2DFA94A0646DB73AE767CA57845966A39BF1356AFDEFFE32D887BA3169233EB20C1108A1D6B89BE52747EE71C4E56B1333775C6ADE0F5426FD7B5995C5B18BE475787A8F06D8ED8B704021B1A459E78E387CFDA47220858EC79831855438FA0EADD73DE3920B2E938A897BEFDE3F4ADF77E81675F9599493C3497A34FD2A3633A170E8BEBD01BE304E6E323C51A73B106CEF1FDC8986793C3970A9A291A9A5162B1BF01DB167C01FD998CEF84797E72487A4C01E1", + "key": "D9C3C43B84F2CA1A97BF1AE429C161EDD233518B2A3DC1E2004AAD91F8B1B876", + "pt": "0F00000000000000000000000000000000EA4129D52CE10A617173FCD6385EF92E6B95A9E6B0499344A8AC62BC2393D282465FD2C14B363788FE4149BBDB04D44A6D26CE574D2F636ACD4E7B412FC92D7FD570B07738D4B87EC4B2BB7D88C24CB057666BAE116A24C593632737D27B816E2F5C1D3BC635764F71295F4A9359587B9287C81870CC544287D6A4F975F62CE439875638846147FDC8176963DC54C5C873805B71EDCCBF405F3F0CC66ECBF37631323852ED55C701FBD865A2F48D9DE9DD67BCB186372CBDE7A6A2FD35FDCDD4707403CD30354BEC0CFD59F4508D1149DFD48E04B3062C2B7BA67F61643843C190F714E3B5DEF17FBD0C721048F8783B16E4856232A40E9D69748298509CD3661C3E698605A124D76F5E310102ECFE1DACDEB4387AB255999749002477ED05244872388CD1FFA3B9C6A79FB1ED377B71F8C8C3B2FEB220100FC6A9D3426905BC825A578AB7EE91ACCC437A736EA76A07C5AA125131CE2957032D0777649015FE35840DAF50C49CC50B5A4AC1620063078192A142E827963F93AF2D23D6F7BE05CB39CFB2AA5B8F8FA423D73B720292ACFC56804391C26188A2A3FBE1EDF824E9DD6928E29D0BB76D73EB33151DA552CD0A307C8394CCCBF163FF958539E257AFA2EA924F11B933281ADD128B1C987917FD44EEA4BD57F4327A2561FD75B91F522A9EAEB93FA87B0BB304C8FA4380DF66", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 18`] = ` +{ + "ct": "0F0000000000000000000000000000000029D6C4FB2524D316DB175938B64F3A772C86A770928A23CEFFAA8C24891861E9772A465EFA9389D008FC7C9A9A0CDBAD1FECE20140E85023AF4A4A1CA4F2A211D2A0F07110A0FA533DBC623B45067B49E115D8E0CAF5AAB8158BED7DBA04CFD50872ACB1620522FEACC0DE2781E70B2BC1229D7169D30357678E64DA938FA327E72F1013EA52B69732BEBC99AE7D5016123A9CEA0C9BDCAA82029E8B90EF04FC30708D78EDE3576A3B6C7C2369184EC7D14B80C1055F835390F3885FB6F29C8201301D66F72A852E8A023DA130664139417AFA1F50B67023DC5592E6D8951A718E8C96A3FACD5049F606FA894965FE97562F1E29761AC703B54FBCADDEF70FCA439EE805606CFAB5B42A527477A3597B9AF55B181BF070295132DBB7E2C9A97D6ADB8BF9022644604E2B4543C0303507CEA0E338EF5E85693012265BF8ACEF93BBA6487FFFF0426B78B31EC3BCBBB4DA310E6707419D484A6E803310A39A467BBD7918FF3639743168812C8D3AA86FF0268CBC53895D1B2F2F7D270039CD9928C3D6D3101B1EBB2967572DF534622199F855F21E376BBC34C8F2CB215FC2DFA46C927B7528446CE29167C3A62175A5CB3BB8BF1C2C1A11F934B9D5A12BE9B4B1AA9AD11CC77AF12E47D4DDC21165F6D6031B1147432E8DC2075A4A856B6C92F598733EA37E6AE39267DB370409934A5C219D168E2AD3D171D433261B4D3270F9B8E4666E5A1BE51E5064E81918FDBEE0305263F81BBAD1DAB484079E5C8082A94B7BB111A32C0F17F4D117BA63D5BD25", + "key": "D9C3C43B84F2CA1A97BF1AE429C161EDD233518B2A3DC1E2004AAD91F8B1B876", + "pt": "0F0000000000000000000000000000000029D6C4FB2524D316DB175938B64F3A772C45309DBC83E6178800E8816796702A2E6D96C7819F43A55BDCD010A748DE0FB784D0454622F95782EFD7850F8EEE1B977A526F02ABF7C900F3C39781263C8A8557D202D2CFD5F7A24FAC05500D2BA532A09D08CEC6870F9EF1F2ACB3657D810A3F61F15ADBF0A62761BC7775AF9F021DB7A8BA0378A133D61E0B81207D09A5F41EAC148E1CC54C4C1C97E972132D305DF7138F88808710377F5FB846049619972745EAD83A5613813F2F65AC401589F17A5D9FAC7488542464A42CA8FFFCE11CFBF441022A31E0647D0B1B006D689C264FB9D9BF7C7C543D1921FD3301ABBC0B99A6844C10658E9ECFA9FE338CF619C483952686124EDEA7DDE95F11C0495EE52DE6E4BB73C34358FA586FBCDA85CA299278ABDEE785F694E4F61B8BFB028720C10378C8519D4F7F40653252CA23EA4D7E41BF35C6A1D627C3980F5DAB17FC67C14311741F607A146BB3F2DFA94A0646DB73AE767CA57845966A39BF1356AFDEFFE32D887BA3169233EB20C1108A1D6B89BE52747EE71C4E56B1333775C6ADE0F5426FD7B5995C5B18BE475787A8F06D8ED8B704021B1A459E78E387CFDA47220858EC79831855438FA0EADD73DE3920B2E938A897BEFDE3F4ADF77E81675F9599493C3497A34FD2A3633A170E8BEBD01BE304E6E323C51A73B106CEF1FDC8986793C3970A9A291A9A5162B1BF01DB167C01FD998CEF84797E72487A4C01E1", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 19`] = ` +{ + "ct": "0F00000000000000000000000000000000D6D1619499AB85AE9080EC1BF24A61FF2D2E643DB6BE398409BA6F35039E6EA1EB3D54E4BB935067118470CC91300179E9D5820309ADB70F3F9F27965A52E0AABB6C5E50D7AE32547934CEB45A9EB645954C947BFC21F7BCEEB5E47272CBF3F585248BC4EF161A53F0A93D7A71F29F773D1816EDE7DDDE8DE1FD1280A8AEEBCFC3B314851C1292C7B9AD3D84E6919E743ADF1D04080803EF60C7DA0A1F923746AE5D307EF7CBFEC25F018EDAEFB18AC947DA59ED99C5205A70358EAB2D28265A3E38C12598DBD957A4748770379B7AEA2BBE2CF7DF2182DD7FF405D77231000830BCC38CD0CE064263E5DAA5C3DB21E1675430364997D846D5EE9C17070D192484FEFE4F9CBE8F207EA5940C63420260ED541036D2B95783DE99CF341891BBD9F69F28F452960C423DE6BCC3134154068F40084B9593C81104BA91DD70FB4C8703DF6E64C7D0B69C16AED96FE1062AA9F2BB52DE95CF4D74380ECC64873662071D075F8C3052D172852C9FED55FED59FC95353C8669950ED24C2BA526A1A4973E81A54C882F69979872B54A946E86BD3A42A13391D3E8D8B95A0788AA5822E392DE2C5402CF0CCD276B411E2459C735EC65FD0F85678DEA8D924E297DB68A60F9E4E6894BA43E0F7BBA5C9E258420AF537DD6A40D69E17FDF4B4B7E850E56CCB4FA20C35334A787FEDD5EF47370ABD5B2F565BC8BD3B539B29D228FA23EA7D74FE9854A942728FB3AB95BF9A9DE9E8B3ABA87766595F724CFB2A45DA78EFBE4EF733C2ADFE26B788135CCCFD94C6F9B0B36D813A7E006006E855430EEAB27EA4D1B485976E77C8CDBE8319871E0A1143", + "key": "986793C3970A9A291A9A5162B1BF01DB167C01FD998CEF84797E72487A4C01E1", + "pt": "0F0000000000000000000000000000000029D6C4FB2524D316DB175938B64F3A772C86A770928A23CEFFAA8C24891861E9772A465EFA9389D008FC7C9A9A0CDBAD1FECE20140E85023AF4A4A1CA4F2A211D2A0F07110A0FA533DBC623B45067B49E115D8E0CAF5AAB8158BED7DBA04CFD50872ACB1620522FEACC0DE2781E70B2BC1229D7169D30357678E64DA938FA327E72F1013EA52B69732BEBC99AE7D5016123A9CEA0C9BDCAA82029E8B90EF04FC30708D78EDE3576A3B6C7C2369184EC7D14B80C1055F835390F3885FB6F29C8201301D66F72A852E8A023DA130664139417AFA1F50B67023DC5592E6D8951A718E8C96A3FACD5049F606FA894965FE97562F1E29761AC703B54FBCADDEF70FCA439EE805606CFAB5B42A527477A3597B9AF55B181BF070295132DBB7E2C9A97D6ADB8BF9022644604E2B4543C0303507CEA0E338EF5E85693012265BF8ACEF93BBA6487FFFF0426B78B31EC3BCBBB4DA310E6707419D484A6E803310A39A467BBD7918FF3639743168812C8D3AA86FF0268CBC53895D1B2F2F7D270039CD9928C3D6D3101B1EBB2967572DF534622199F855F21E376BBC34C8F2CB215FC2DFA46C927B7528446CE29167C3A62175A5CB3BB8BF1C2C1A11F934B9D5A12BE9B4B1AA9AD11CC77AF12E47D4DDC21165F6D6031B1147432E8DC2075A4A856B6C92F598733EA37E6AE39267DB370409934A5C219D168E2AD3D171D433261B4D3270F9B8E4666E5A1BE51E5064E81918FDBEE0305263F81BBAD1DAB484079E5C8082A94B7BB111A32C0F17F4D117BA63D5BD25", +} +`; + +exports[`Serpent > Cipher must match reference test vectors cascade 20`] = ` +{ + "ct": "0F00000000000000000000000000000000D6D1619499AB85AE9080EC1BF24A61FF2DD16398D902B6D2B1F1F88020DA6BFA633CFC27F6B7647D5B72602F80BA8776A1499511B3ECB7D688865F9A0C59DC706F9A6730D5E777B355A41BD9D06016072E2558D5DDE6F974E9F16CDEFDD46B388950D2887420CE0EF45203737E3A504B1D9540CA9250CDB54CA17B63F55EFE5B9F4F2E0E809C9C1A632B4EF2BCAABF3CD2FB81009EFE43CB304CC98B02DADB88CC75D5FF19CF6D1D1AE34B9E6E3EF7FAFD4474509FDF08F3D851E359D4AE8F9E30AE50FCB44743F4AD944176C300A050F381242B9C4E738106DA736DA336FD33E3D6075A384191B09D467B32F27185FB1EEA006366BDDB10DE7B7F372B87A7FBF2CF85618381023F3BC5352CD99BF87603937020984585F64B5340F260855A82EDE889BF7C5F4565976917D790BB0C142BDC4CE898C3785926C95C24F0283DE6029E8E8FB4AA7BD16E9A71D8678E7D0AE6032BF88EF76D80CF60D1A32B6BDF996EC4EAB9E9868377BB7A111221DA79BBB5F32D8F159F3B48672F17E10A751FC987F58B2719195D324520EB3414BBBC03FF01DFBEC5A765A9C54004F3F9F0A47367BEBAA10BDA99E3C9E94351EE6633AF5B1EA046637C6A08E9C331459F0C4FD90BB38FFB5E398238EA4854D56EB28BE789E9BEF95EE32EEC2D48C6FC70AC4B376C96CA4E34DFC8E5D22601BA5A3134A511818AFA2B767300C458262260D4E483F9969FE404E5673141685E947C8730BE26EE0B549E471B2656D1B48DFB6AAA18A4A838B96064522B851362BFD0E2968C54D1C949C25D6ABE77B2CED5AD456DE04407E8165409C8ADB86B4CC410E0A33D461A08BD47D71EDA80D35CABAF6686583F7CE859CCC2F6EACC1AA414B01596F4", + "key": "986793C3970A9A291A9A5162B1BF01DB167C01FD998CEF84797E72487A4C01E1", + "pt": "0F00000000000000000000000000000000D6D1619499AB85AE9080EC1BF24A61FF2D2E643DB6BE398409BA6F35039E6EA1EB3D54E4BB935067118470CC91300179E9D5820309ADB70F3F9F27965A52E0AABB6C5E50D7AE32547934CEB45A9EB645954C947BFC21F7BCEEB5E47272CBF3F585248BC4EF161A53F0A93D7A71F29F773D1816EDE7DDDE8DE1FD1280A8AEEBCFC3B314851C1292C7B9AD3D84E6919E743ADF1D04080803EF60C7DA0A1F923746AE5D307EF7CBFEC25F018EDAEFB18AC947DA59ED99C5205A70358EAB2D28265A3E38C12598DBD957A4748770379B7AEA2BBE2CF7DF2182DD7FF405D77231000830BCC38CD0CE064263E5DAA5C3DB21E1675430364997D846D5EE9C17070D192484FEFE4F9CBE8F207EA5940C63420260ED541036D2B95783DE99CF341891BBD9F69F28F452960C423DE6BCC3134154068F40084B9593C81104BA91DD70FB4C8703DF6E64C7D0B69C16AED96FE1062AA9F2BB52DE95CF4D74380ECC64873662071D075F8C3052D172852C9FED55FED59FC95353C8669950ED24C2BA526A1A4973E81A54C882F69979872B54A946E86BD3A42A13391D3E8D8B95A0788AA5822E392DE2C5402CF0CCD276B411E2459C735EC65FD0F85678DEA8D924E297DB68A60F9E4E6894BA43E0F7BBA5C9E258420AF537DD6A40D69E17FDF4B4B7E850E56CCB4FA20C35334A787FEDD5EF47370ABD5B2F565BC8BD3B539B29D228FA23EA7D74FE9854A942728FB3AB95BF9A9DE9E8B3ABA87766595F724CFB2A45DA78EFBE4EF733C2ADFE26B788135CCCFD94C6F9B0B36D813A7E006006E855430EEAB27EA4D1B485976E77C8CDBE8319871E0A1143", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 1`] = ` +{ + "ct": "00FFFFFFFF000000000000000000000000000000000000000000000000B438D1E280FA298FE868FE5A2A3C8D48", + "key": "0000000000000000000000000000000000000000000000000000000000000000", + "pt": "00000000000000000000000000000000", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 2`] = ` +{ + "ct": "03FFFFFFFF000000000000000000000000000000000000000000000000B4C72E1D7FFA298FE868FE5A2A3C8D48C4938986E1893744C89619E5FAC7CD36C0AD210AE7320C074BA782871C584E27", + "key": "0000000000000000000000000000000000000000000000000000000000000000", + "pt": "00FFFFFFFF000000000000000000000000000000000000000000000000B438D1E280FA298FE868FE5A2A3C8D48", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 3`] = ` +{ + "ct": "03FFFFFFFF0000000000000000000000000000000000000000000000000008BDEA3137A6026EFCB95FC98A094C201F88CC895E775A07553382535F58056304E9C87D4FD21173DADF3ACD18309F14BD122562A34744A993BCFCF8621D7B35AAC5F5201E9B812DD31C8B5A356A6A", + "key": "00000000000000000000000000000000B438D1E280FA298FE868FE5A2A3C8D48", + "pt": "03FFFFFFFF000000000000000000000000000000000000000000000000B4C72E1D7FFA298FE868FE5A2A3C8D48C4938986E1893744C89619E5FAC7CD36C0AD210AE7320C074BA782871C584E27", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 4`] = ` +{ + "ct": "03FFFFFFFF0000000000000000000000000000000000000000000000000008BDEA3137A6026EFCB95FC98A094C201F88CC895E775A0755338253EB9796944A2447F0C94656763969BEC9FCBC9E5ED5C5657C6C846ECE3A2469CBC1B4B3F730B82B3626E6DC90025CF5E221D77878C56205AA1B3348334CF6BE79203FF934ADD066FF0DD114E9C3B9B31710E68E", + "key": "00000000000000000000000000000000B438D1E280FA298FE868FE5A2A3C8D48", + "pt": "03FFFFFFFF0000000000000000000000000000000000000000000000000008BDEA3137A6026EFCB95FC98A094C201F88CC895E775A07553382535F58056304E9C87D4FD21173DADF3ACD18309F14BD122562A34744A993BCFCF8621D7B35AAC5F5201E9B812DD31C8B5A356A6A", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 5`] = ` +{ + "ct": "03FFFFFFFF000000000000000000000000000000000000000000000000D893C61B11490F5EA025E300F97D96A143358E5B9D3FE5400D6ED7B82C82DF199B55DE91F98B26C9030397EC64670709E626580EBDE30AF43249A83E4053789A9498514F64B2044C48011FC8BED1F4385D7A04CD6406AD1D40626C8F73AF096E2F5A03F5873917794209842FF44DA1A5A65AEABCE0BE98E21A4517AC03F2C352607325E7E99DD55F7C37FF41C2A34236", + "key": "14BD122562A34744A993BCFCF8621D7B35AAC5F5201E9B812DD31C8B5A356A6A", + "pt": "03FFFFFFFF0000000000000000000000000000000000000000000000000008BDEA3137A6026EFCB95FC98A094C201F88CC895E775A0755338253EB9796944A2447F0C94656763969BEC9FCBC9E5ED5C5657C6C846ECE3A2469CBC1B4B3F730B82B3626E6DC90025CF5E221D77878C56205AA1B3348334CF6BE79203FF934ADD066FF0DD114E9C3B9B31710E68E", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 6`] = ` +{ + "ct": "03FFFFFFFF000000000000000000000000000000000000000000000000D893C61B11490F5EA025E300F97D96A143358E5B9D3FE5400D6ED7B82C5A44626A75A038A545FF935C33607389042D0F7132399CA7E931100836C176CF5C67604291132FFBC73EB21AAC8473296907A536BB8B43FEFADE9117E9FE435ACCA1874B08971717E1143A7F55740CB4681EC36E94F722B5CDB6782B4F989A94E93481F30B112184361F62E0D4A206E90518DC6D02044E6FE2800EA99C258AF6C92436DE9C5D94EB4B154576570C5A612AB8C6", + "key": "14BD122562A34744A993BCFCF8621D7B35AAC5F5201E9B812DD31C8B5A356A6A", + "pt": "03FFFFFFFF000000000000000000000000000000000000000000000000D893C61B11490F5EA025E300F97D96A143358E5B9D3FE5400D6ED7B82C82DF199B55DE91F98B26C9030397EC64670709E626580EBDE30AF43249A83E4053789A9498514F64B2044C48011FC8BED1F4385D7A04CD6406AD1D40626C8F73AF096E2F5A03F5873917794209842FF44DA1A5A65AEABCE0BE98E21A4517AC03F2C352607325E7E99DD55F7C37FF41C2A34236", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 7`] = ` +{ + "ct": "03FFFFFFFF00000000000000000000000000000000000000000000000088D08877F8F7A8EC826084EC411508EFD942CF156C331957E20DAA235DAF0534FCBA698815D2DDD2B9168FD4AB191556BFF7CD4FB34326083F88A854DDF132134228165C3E344E46CC4E68EB016AEABC72D281E3A86ACE73B6993896500186526B06AD8C8738C2F4F4091E6D933154E96F11DF9E5EC92C7BCA8554D751E3D22C661D589642A7986F6AD0AB0EA4E07C7238169CFF551F50C8627BC3562BAE4B845EBE6832FDFCA04CF82EF640DF532EA8DA2D55BE12B2E2A80107626AA62F6292B8A80B060FFE60E7D09BC93E112A727D", + "key": "A65AEABCE0BE98E21A4517AC03F2C352607325E7E99DD55F7C37FF41C2A34236", + "pt": "03FFFFFFFF000000000000000000000000000000000000000000000000D893C61B11490F5EA025E300F97D96A143358E5B9D3FE5400D6ED7B82C5A44626A75A038A545FF935C33607389042D0F7132399CA7E931100836C176CF5C67604291132FFBC73EB21AAC8473296907A536BB8B43FEFADE9117E9FE435ACCA1874B08971717E1143A7F55740CB4681EC36E94F722B5CDB6782B4F989A94E93481F30B112184361F62E0D4A206E90518DC6D02044E6FE2800EA99C258AF6C92436DE9C5D94EB4B154576570C5A612AB8C6", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 8`] = ` +{ + "ct": "03FFFFFFFF00000000000000000000000000000000000000000000000088D08877F8F7A8EC826084EC411508EFD942CF156C331957E20DAA235DFF467A9053D72FA7F098B555AEE74AE5836217F106C1B3A4AC4575A4F95D158B67FDDAF298817E7FD16BA96B6C75D358A42F48A1C62BF4B05D701A948B95C323013F5718C35EFC73EE20186C211D808A753DE3CF474F8EBC685CBD1F8F99F084C3DC16FD8D81408C2CC4050BF7F2448EE1F95A84FD986556FE9A042FBEC9B0863B5DCDE978F9B5F076A445F063132471063A306B17A86ED479054EDDDA050514AF40A71EBEBCB30670191DCA25B0A87FF05F286EAB4719E0277883C74249DEAF6C9269732D4D61642D64B0A7EC51CAB8395E1B", + "key": "A65AEABCE0BE98E21A4517AC03F2C352607325E7E99DD55F7C37FF41C2A34236", + "pt": "03FFFFFFFF00000000000000000000000000000000000000000000000088D08877F8F7A8EC826084EC411508EFD942CF156C331957E20DAA235DAF0534FCBA698815D2DDD2B9168FD4AB191556BFF7CD4FB34326083F88A854DDF132134228165C3E344E46CC4E68EB016AEABC72D281E3A86ACE73B6993896500186526B06AD8C8738C2F4F4091E6D933154E96F11DF9E5EC92C7BCA8554D751E3D22C661D589642A7986F6AD0AB0EA4E07C7238169CFF551F50C8627BC3562BAE4B845EBE6832FDFCA04CF82EF640DF532EA8DA2D55BE12B2E2A80107626AA62F6292B8A80B060FFE60E7D09BC93E112A727D", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 9`] = ` +{ + "ct": "03FFFFFFFF000000000000000000000000000000000000000000000000E31FEC6F75B5A07ABC229025BCFFC3771077EF9CC37136095DEC5E45D1031D55841CCC1B0A24CEB62D60F426B6A54B41916563E87567C74D70BA7B3864DC433098049EBDC7814F4ABD636D97202E2A59889DC3F308FB132A9748286A5FDA0F3BBF88D0D74862C78C11C6D4CADF7CB933AD4926D1F6F9D1BE10C6E75A1FF52A7C16F6366B63639CAC96AE8CC39413102BAC8CB6CB82F0CE84DF9FAF64B5AD48655353D4DD6EA6562DCBF01B841D85C3740E1DFFA514A0513DBED3FF4E5149A560E556F8A73B95E5B54F7021295F93E48F7238841F1F46399D4FAE27895224D811BD7EA76CEC7F63E3E6F13C1AE5096DABBF1B0CCC6CFDC6DED5D86AA9D9B76DE2C179B4321BF43D708E9A1857B55B15CA", + "key": "DA2D55BE12B2E2A80107626AA62F6292B8A80B060FFE60E7D09BC93E112A727D", + "pt": "03FFFFFFFF00000000000000000000000000000000000000000000000088D08877F8F7A8EC826084EC411508EFD942CF156C331957E20DAA235DFF467A9053D72FA7F098B555AEE74AE5836217F106C1B3A4AC4575A4F95D158B67FDDAF298817E7FD16BA96B6C75D358A42F48A1C62BF4B05D701A948B95C323013F5718C35EFC73EE20186C211D808A753DE3CF474F8EBC685CBD1F8F99F084C3DC16FD8D81408C2CC4050BF7F2448EE1F95A84FD986556FE9A042FBEC9B0863B5DCDE978F9B5F076A445F063132471063A306B17A86ED479054EDDDA050514AF40A71EBEBCB30670191DCA25B0A87FF05F286EAB4719E0277883C74249DEAF6C9269732D4D61642D64B0A7EC51CAB8395E1B", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 10`] = ` +{ + "ct": "03FFFFFFFF000000000000000000000000000000000000000000000000E31FEC6F75B5A07ABC229025BCFFC3771077EF9CC37136095DEC5E45D168D2319C918E139C1A8CA2E49D1EED2E6C7E6118CA21C72BD826B9163687634BC80C2BACA94AEBC4F98159D1304BBE764E49FBD34C0871302F500CBAA793D4B5B093247C3080F3ABB4C89455BE5ECFCE55E2DBAAF180B2C6FA1203B9BA3C6A735261F23DCDBA8CF71E7B65DCFB854744711E42F3C6274681FF87FA75049992DF4633D278BC9B85C73B0F534CEAE96D6CADB25AA0C9F1F19450705B6AE0695BF9F38E4D8DC8285553AE76BDEF1CA2D01BF6EED8B9F85D4B6C25306704EBC16C95DF3055A943429044FA367267D15FA14215FE68B9E46D8D72752AB0822522E3A1793E08CCF1E63548B5201D5EC7286405E40EC6E67791A0AB653CBB4DD17271E90397CCF974BB11FF7E8515EDE12694EC9D1A93", + "key": "DA2D55BE12B2E2A80107626AA62F6292B8A80B060FFE60E7D09BC93E112A727D", + "pt": "03FFFFFFFF000000000000000000000000000000000000000000000000E31FEC6F75B5A07ABC229025BCFFC3771077EF9CC37136095DEC5E45D1031D55841CCC1B0A24CEB62D60F426B6A54B41916563E87567C74D70BA7B3864DC433098049EBDC7814F4ABD636D97202E2A59889DC3F308FB132A9748286A5FDA0F3BBF88D0D74862C78C11C6D4CADF7CB933AD4926D1F6F9D1BE10C6E75A1FF52A7C16F6366B63639CAC96AE8CC39413102BAC8CB6CB82F0CE84DF9FAF64B5AD48655353D4DD6EA6562DCBF01B841D85C3740E1DFFA514A0513DBED3FF4E5149A560E556F8A73B95E5B54F7021295F93E48F7238841F1F46399D4FAE27895224D811BD7EA76CEC7F63E3E6F13C1AE5096DABBF1B0CCC6CFDC6DED5D86AA9D9B76DE2C179B4321BF43D708E9A1857B55B15CA", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 11`] = ` +{ + "ct": "03FFFFFFFF000000000000000000000000000000000000000000000000C9D69210E18FEC2A77BF66996CBC97043332CF356C9F9A21602D26C1DA1111CF584AE104FECA0AA0E55EEE56DACDFE970A3FBCA71F2669BA14C1252F262DB4BEBC0A35C11D688709513CA67957BDDD61377996E67077494EF613B0A8ACB4E60756BCAD56BFF1C9EA3EEAD04CE1AFA0887C5ED0FE9B5947CD1588E7E5FDCFF30A765DB42724C0304DB419475301A25A67C6A2C83F7DFBE53B59D7722DE2419EAC080D4312205FD158F52E42C0AF0DCB8FD47EE544ED5247AEC09D563989704AFCB48E338FB40949B4176EDB04795FE5C107290990553614DBAACEDA126F906FB3A3ABD8A4EDB596AC3E2567918218E302111184A40929C6B4FA26A77BF2196FB7CE87EDC1290C610DED46A46B9727A742B24D4D3AB6BD33B76F98D12E80FC81486D48B79F4CE7CCC8493ED1D452E6FCC91E65ED1A9EB5B797B85047DF1F1FD252F2C04AA86EE9CB07D5BF4E64B534DBB9", + "key": "BF1B0CCC6CFDC6DED5D86AA9D9B76DE2C179B4321BF43D708E9A1857B55B15CA", + "pt": "03FFFFFFFF000000000000000000000000000000000000000000000000E31FEC6F75B5A07ABC229025BCFFC3771077EF9CC37136095DEC5E45D168D2319C918E139C1A8CA2E49D1EED2E6C7E6118CA21C72BD826B9163687634BC80C2BACA94AEBC4F98159D1304BBE764E49FBD34C0871302F500CBAA793D4B5B093247C3080F3ABB4C89455BE5ECFCE55E2DBAAF180B2C6FA1203B9BA3C6A735261F23DCDBA8CF71E7B65DCFB854744711E42F3C6274681FF87FA75049992DF4633D278BC9B85C73B0F534CEAE96D6CADB25AA0C9F1F19450705B6AE0695BF9F38E4D8DC8285553AE76BDEF1CA2D01BF6EED8B9F85D4B6C25306704EBC16C95DF3055A943429044FA367267D15FA14215FE68B9E46D8D72752AB0822522E3A1793E08CCF1E63548B5201D5EC7286405E40EC6E67791A0AB653CBB4DD17271E90397CCF974BB11FF7E8515EDE12694EC9D1A93", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 12`] = ` +{ + "ct": "03FFFFFFFF000000000000000000000000000000000000000000000000C9D69210E18FEC2A77BF66996CBC97043332CF356C9F9A21602D26C1DA3BD8B127DEDB48AE019756598EAD02A9EEBBB7A390520B371BA8C290CA5CECD8E96FD1AB68E5471F6944F9EAC807F9A1AF28FC574D68A97375BEECBA7E551039A445787C653C50EF71C507F9CB23D87B4B9516EB1E88E7D915F3EE6991E390DEE57F27D349F1265AAB64C33736BD115274F5378AFF6B6AB357D7E0D659EFBF1A0A0EA2A3DE9D590837339AE16B91848A38AF60AD82E1262C7E94457FA05194F700FB2423695751BFB08D1FBAAD7B7DAC2B52D5B2500BA7A09C69E4C8BE59D6DEA929A8794C7F9BE44DE4D5EA47CE9A9BA63256CA289780B587E3AF84DCE8241413872DFA2EA261F7559535DD86A976FD8F52AB9B3611D6B2C53032F7CBB17F3FFEF74399290EDE5C544D4446AC127DD6B2C615847835159297FE34E739B8C99B8BEE5E7C73D3E1B34D14F095013582EF2ABE54EEDF2CD9C641376E2F6761B35D7AB8CCA167CFAAD42975372D0423A87E2F0957", + "key": "BF1B0CCC6CFDC6DED5D86AA9D9B76DE2C179B4321BF43D708E9A1857B55B15CA", + "pt": "03FFFFFFFF000000000000000000000000000000000000000000000000C9D69210E18FEC2A77BF66996CBC97043332CF356C9F9A21602D26C1DA1111CF584AE104FECA0AA0E55EEE56DACDFE970A3FBCA71F2669BA14C1252F262DB4BEBC0A35C11D688709513CA67957BDDD61377996E67077494EF613B0A8ACB4E60756BCAD56BFF1C9EA3EEAD04CE1AFA0887C5ED0FE9B5947CD1588E7E5FDCFF30A765DB42724C0304DB419475301A25A67C6A2C83F7DFBE53B59D7722DE2419EAC080D4312205FD158F52E42C0AF0DCB8FD47EE544ED5247AEC09D563989704AFCB48E338FB40949B4176EDB04795FE5C107290990553614DBAACEDA126F906FB3A3ABD8A4EDB596AC3E2567918218E302111184A40929C6B4FA26A77BF2196FB7CE87EDC1290C610DED46A46B9727A742B24D4D3AB6BD33B76F98D12E80FC81486D48B79F4CE7CCC8493ED1D452E6FCC91E65ED1A9EB5B797B85047DF1F1FD252F2C04AA86EE9CB07D5BF4E64B534DBB9", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 13`] = ` +{ + "ct": "03FFFFFFFF000000000000000000000000000000000000000000000000439A63469DBA7BD0877222DDBAC5288E8BF3AAAA68A99F1F1ED38E0DE92CD2CE9B3907791FD82971ED8D9E68321DFD5CEB5CC5EF4ADD7DACB2F9EE5076C1FC8A46E7C4E8BDC8F731E26D880458B543F3848697478E06C6B580F156049AECEDC0CF5302C31F4D19827571C6E6C232B17409548E5E95F8C27AC68F304D8AF6BE63D972DD7AEF8991F5DC4A7D87539B906E6310704047125714347B691374E6EDEA1F2A026D5E4C3E1CE48C848FF40541D7F98B33B5FB174147DA18F77695F9A9D23B9DBC7F84AF809680B007ED8F87F167FEDF9BEF72C04D850BF452952FC9691C082BA620F6A366F08AFE16D0D75BF5A3225FE225A73595B33A86BCDAD15391CF17D8B87B9441FCEA9151071E524CBEF8C0E5D647D5B788B00BE140A0A18A9A79284F72981336FC58F624CFD7B000CB202F494663FA15F4C7BC4F995082071839923A4A48C83187721D54AFE4208C9C023449C68EB87AE51F34ED313411F0A1BE2359CE44D0B511F224CB16ED770D8D86469013A8E88CAF090ED68622E3F7BFDA1B06FD8E6DD6BEDDE248BA503ECE4036", + "key": "1E65ED1A9EB5B797B85047DF1F1FD252F2C04AA86EE9CB07D5BF4E64B534DBB9", + "pt": "03FFFFFFFF000000000000000000000000000000000000000000000000C9D69210E18FEC2A77BF66996CBC97043332CF356C9F9A21602D26C1DA3BD8B127DEDB48AE019756598EAD02A9EEBBB7A390520B371BA8C290CA5CECD8E96FD1AB68E5471F6944F9EAC807F9A1AF28FC574D68A97375BEECBA7E551039A445787C653C50EF71C507F9CB23D87B4B9516EB1E88E7D915F3EE6991E390DEE57F27D349F1265AAB64C33736BD115274F5378AFF6B6AB357D7E0D659EFBF1A0A0EA2A3DE9D590837339AE16B91848A38AF60AD82E1262C7E94457FA05194F700FB2423695751BFB08D1FBAAD7B7DAC2B52D5B2500BA7A09C69E4C8BE59D6DEA929A8794C7F9BE44DE4D5EA47CE9A9BA63256CA289780B587E3AF84DCE8241413872DFA2EA261F7559535DD86A976FD8F52AB9B3611D6B2C53032F7CBB17F3FFEF74399290EDE5C544D4446AC127DD6B2C615847835159297FE34E739B8C99B8BEE5E7C73D3E1B34D14F095013582EF2ABE54EEDF2CD9C641376E2F6761B35D7AB8CCA167CFAAD42975372D0423A87E2F0957", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 14`] = ` +{ + "ct": "03FFFFFFFF000000000000000000000000000000000000000000000000439A63469DBA7BD0877222DDBAC5288E8BF3AAAA68A99F1F1ED38E0DE9A69E3FCD4532EEE528E435A95BE7D7B8A53C397458F3EA74A383047ECAF95A097D1B5677561D569A7CF40288F67B42B3FD8F6460FB5192E024F5073C5F7E97C10162E160F1A370D745BC0D8888DC8DCDE17A8BE7A9FD26CCC24D79D22C78E53245885D4A82E1A66A052B10E2F304A33179DA68DA5C9D71D3BD49C7E92F7AD230ECD6C6439F209868A742DC728D6BEAADECAECCD37F76350FF563C176761B95DD455D4D0FCBC77202AA67838BCE3A0338D3F8B56D08F23A7065F523E996ABC7D9D19DF72610B92D7F997B8C1ADDBA7365177A336A8DBE01C6F6DFB87977DC9A6522F616ACCA56F9B1214532DB1DFAD9A7A4C98D65F76431C909D2E4F52400B643676C63322C66F1CC7A2BF69E8B0C3BE35B18E7BE2E34DB78E9DE3663D1ED3DB8B67E45D47528F9D4810FAFB7321DE91527AD71422144844B21933EADF4BDC2761EE8278A22B2D7523D44684204B0CF21D71B6C11EE287A99972559899A0C3B917581DBF50261EAA8DF71E8A74198D4EF88D0254397B8AD376AA525206E039D6505F26F576121582D066023B9221FD31D5B74AC", + "key": "1E65ED1A9EB5B797B85047DF1F1FD252F2C04AA86EE9CB07D5BF4E64B534DBB9", + "pt": "03FFFFFFFF000000000000000000000000000000000000000000000000439A63469DBA7BD0877222DDBAC5288E8BF3AAAA68A99F1F1ED38E0DE92CD2CE9B3907791FD82971ED8D9E68321DFD5CEB5CC5EF4ADD7DACB2F9EE5076C1FC8A46E7C4E8BDC8F731E26D880458B543F3848697478E06C6B580F156049AECEDC0CF5302C31F4D19827571C6E6C232B17409548E5E95F8C27AC68F304D8AF6BE63D972DD7AEF8991F5DC4A7D87539B906E6310704047125714347B691374E6EDEA1F2A026D5E4C3E1CE48C848FF40541D7F98B33B5FB174147DA18F77695F9A9D23B9DBC7F84AF809680B007ED8F87F167FEDF9BEF72C04D850BF452952FC9691C082BA620F6A366F08AFE16D0D75BF5A3225FE225A73595B33A86BCDAD15391CF17D8B87B9441FCEA9151071E524CBEF8C0E5D647D5B788B00BE140A0A18A9A79284F72981336FC58F624CFD7B000CB202F494663FA15F4C7BC4F995082071839923A4A48C83187721D54AFE4208C9C023449C68EB87AE51F34ED313411F0A1BE2359CE44D0B511F224CB16ED770D8D86469013A8E88CAF090ED68622E3F7BFDA1B06FD8E6DD6BEDDE248BA503ECE4036", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 15`] = ` +{ + "ct": "03FFFFFFFF000000000000000000000000000000000000000000000000DFF1474581F0DFD1099996797B41282E632C9361817AE966C97813E574520BCC1982DBB9DC4DCAB5B8B11CBF3D99D89992E8CF2B1F2A82E4A8ABBC6A84EC5842127817718FADA82A3AEDD57877704DBDB9904BE0EE563B3B892D27EABD4D43E185845703D110A3581B232554EE5CAA8838634D19A7D6937C399201ABF7C3980B246A55B2A02D8B625245FE58591BCA3B994BEADCADFBA40E7C84C90FD1AE0A916E01DDBC8CCA5EF1C7663955CDC0DE60089EF3049B1ABDB965DA5A410995CDBDB1B06E60DEDF1855E94FC7B5160E882A6D5827268D5B6DADF61351833B5F1315C5651AC497F1181CF40C08072995B1A53E177CA8C73B295D88BF5480A6F854F8FF813BFDC3DD6A00A05F037DB2F529DFE92EDC00D734B646BE610684FAE8DEF2F08E9308A6BC2A16467604652E3707216A964332595DF2C05B7119EBAD01374FB908CCF1D1CC700C81C0A4FE816CF4B2D1478A1048E22E9114181C2FDC2D3FDBBC0A7045C3F986F95F6AE5D84D7943A28D4F4369D54845F9189ACD568F7EE7FAA4D17A5F6EACD1E77F2EF40A75766FCCD8886278EAFB8F087E752EC1B2B32C5B1743B6E6F6FA1103C1DD046E697B4B135538573B941A9BECD69F142E256827A97D101AECCC148ABCC4EF2E7DE7E48D3E", + "key": "469013A8E88CAF090ED68622E3F7BFDA1B06FD8E6DD6BEDDE248BA503ECE4036", + "pt": "03FFFFFFFF000000000000000000000000000000000000000000000000439A63469DBA7BD0877222DDBAC5288E8BF3AAAA68A99F1F1ED38E0DE9A69E3FCD4532EEE528E435A95BE7D7B8A53C397458F3EA74A383047ECAF95A097D1B5677561D569A7CF40288F67B42B3FD8F6460FB5192E024F5073C5F7E97C10162E160F1A370D745BC0D8888DC8DCDE17A8BE7A9FD26CCC24D79D22C78E53245885D4A82E1A66A052B10E2F304A33179DA68DA5C9D71D3BD49C7E92F7AD230ECD6C6439F209868A742DC728D6BEAADECAECCD37F76350FF563C176761B95DD455D4D0FCBC77202AA67838BCE3A0338D3F8B56D08F23A7065F523E996ABC7D9D19DF72610B92D7F997B8C1ADDBA7365177A336A8DBE01C6F6DFB87977DC9A6522F616ACCA56F9B1214532DB1DFAD9A7A4C98D65F76431C909D2E4F52400B643676C63322C66F1CC7A2BF69E8B0C3BE35B18E7BE2E34DB78E9DE3663D1ED3DB8B67E45D47528F9D4810FAFB7321DE91527AD71422144844B21933EADF4BDC2761EE8278A22B2D7523D44684204B0CF21D71B6C11EE287A99972559899A0C3B917581DBF50261EAA8DF71E8A74198D4EF88D0254397B8AD376AA525206E039D6505F26F576121582D066023B9221FD31D5B74AC", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 16`] = ` +{ + "ct": "03FFFFFFFF000000000000000000000000000000000000000000000000DFF1474581F0DFD1099996797B41282E632C9361817AE966C97813E574CE60E81A9E911DDDC321011C7098BF9D7107A059011C5D66FD2979403648FF77389FAB4541725F0FBC42D15268E99CD796FD8178FBC2E10E805A7EB9A0B6A9A9286DEBA291865FF9A2B8F621E7A8963785C1924A6D3FD79B63E12544EE4D8AF726EDFF576C00ADF5BE209B8B6643885AC4008BA620FE02A8101A7732414F1F87C0E2257ACBF51CFE7AE80B3C0E5B459E83C917A5E0B5E9528F160AB83B189D5EB853409554037CF36AF307562FEBC5BAD569AF5CCCC8F8F548C1EC22C78113CBE168BCD7B96FBB419399E14222D577B695E17022EA42304924ACA7CC5DDA0E441B215B1669539E5333BBB2D41381B624A1B31D402F11F632C57ECEA4A2DC2614BB959FF4FC6F2794C768EFE26355857CBBDE995B887E56FB16B7C669C896593CC395BA40620AF031148D04DF0DC8E147B84CC538663E3CBEDA8E65C20DAB66D640423FB40F3D3A60CF744048FEAE818EEA256C194C80D47AF1A958F530FE817348CF383640BE9DFFB1BFB268425A52BBEACEA7CBC4BD184A6A8FC9136B25A793E2FF40A285C546F9227E6F1F47FAD180E054C980E5C611B94480C18A48A2F01128053EC3CBE69DEC6C75A7799B0E4258B1B569023F04D269FCBE229FF167EA824AA91C43E36F54D95E5C6AECAB70AF0182F800", + "key": "469013A8E88CAF090ED68622E3F7BFDA1B06FD8E6DD6BEDDE248BA503ECE4036", + "pt": "03FFFFFFFF000000000000000000000000000000000000000000000000DFF1474581F0DFD1099996797B41282E632C9361817AE966C97813E574520BCC1982DBB9DC4DCAB5B8B11CBF3D99D89992E8CF2B1F2A82E4A8ABBC6A84EC5842127817718FADA82A3AEDD57877704DBDB9904BE0EE563B3B892D27EABD4D43E185845703D110A3581B232554EE5CAA8838634D19A7D6937C399201ABF7C3980B246A55B2A02D8B625245FE58591BCA3B994BEADCADFBA40E7C84C90FD1AE0A916E01DDBC8CCA5EF1C7663955CDC0DE60089EF3049B1ABDB965DA5A410995CDBDB1B06E60DEDF1855E94FC7B5160E882A6D5827268D5B6DADF61351833B5F1315C5651AC497F1181CF40C08072995B1A53E177CA8C73B295D88BF5480A6F854F8FF813BFDC3DD6A00A05F037DB2F529DFE92EDC00D734B646BE610684FAE8DEF2F08E9308A6BC2A16467604652E3707216A964332595DF2C05B7119EBAD01374FB908CCF1D1CC700C81C0A4FE816CF4B2D1478A1048E22E9114181C2FDC2D3FDBBC0A7045C3F986F95F6AE5D84D7943A28D4F4369D54845F9189ACD568F7EE7FAA4D17A5F6EACD1E77F2EF40A75766FCCD8886278EAFB8F087E752EC1B2B32C5B1743B6E6F6FA1103C1DD046E697B4B135538573B941A9BECD69F142E256827A97D101AECCC148ABCC4EF2E7DE7E48D3E", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 17`] = ` +{ + "ct": "03FFFFFFFF0000000000000000000000000000000000000000000000001AB13417D57BB863F339C20635BD3B23E4FAC66E7DD36178E28B902005D197C31D4C243BDE05882D841F91EE529F816DEFF4E159B6ADB3CA484B1FC25A3D0D73A00D5B30C5E334D1B603CB1B2FEEDD550BE4CB3F120377B54E3F489B952F7ED98C94B943EA633574BB5FB5A5A4A266215C73CCDD0D0E80CF615D5AFDFECAB5A576975ED04D8AC0EE99BC5FE17182F77EB83FB300AE36053DE5D5FBB269A2A9588C7ECCA55656B2A14CDEF1DBB8004E9E628EA89E52D9B944168700BA7754B3435BF2731A73F17C62DFC2AE8EC770BD17EEF91A4905E196BA5DE70929EB695D8AB337D94573FFB2A03F3482B4B70123B37F1D5C14AA98E334A55B0275DFF982A1C0C1F18E17AE144075085BADB33B6903614EC44D96BAC2249756094275C156BE180213B22C25FE81E265168DB6956A0F946D544DCA2A30F3B84C45C5A4DF294DFE2DA2CBFC9E322B7E5BC7017CC6105CDADAE13A4F7D5B47089A1EF77A677E4375215721F192FF5EF9958724A07C3E3DD5381F2C83AE62B3A8985906E54CD8F47AAB53ADF2AF6777729BD5DBA67B3D2AD460B9F312B2E038E9304EECE381972999E71CD1784EB0AA8D56A5C44560316C6C9AE02730D3642454BCAB0FC5193E24BD16863EDF0BA2FDA4B4FCAD5E22621AF535A5FD9E10A9222D25B2E1B7DDD382861994964DD78E4DE81C9C48CDDAB785C9564C31A0481E47CC789F8492EAF8D63E5D0E6156EEA274409F71743A3917EC", + "key": "5538573B941A9BECD69F142E256827A97D101AECCC148ABCC4EF2E7DE7E48D3E", + "pt": "03FFFFFFFF000000000000000000000000000000000000000000000000DFF1474581F0DFD1099996797B41282E632C9361817AE966C97813E574CE60E81A9E911DDDC321011C7098BF9D7107A059011C5D66FD2979403648FF77389FAB4541725F0FBC42D15268E99CD796FD8178FBC2E10E805A7EB9A0B6A9A9286DEBA291865FF9A2B8F621E7A8963785C1924A6D3FD79B63E12544EE4D8AF726EDFF576C00ADF5BE209B8B6643885AC4008BA620FE02A8101A7732414F1F87C0E2257ACBF51CFE7AE80B3C0E5B459E83C917A5E0B5E9528F160AB83B189D5EB853409554037CF36AF307562FEBC5BAD569AF5CCCC8F8F548C1EC22C78113CBE168BCD7B96FBB419399E14222D577B695E17022EA42304924ACA7CC5DDA0E441B215B1669539E5333BBB2D41381B624A1B31D402F11F632C57ECEA4A2DC2614BB959FF4FC6F2794C768EFE26355857CBBDE995B887E56FB16B7C669C896593CC395BA40620AF031148D04DF0DC8E147B84CC538663E3CBEDA8E65C20DAB66D640423FB40F3D3A60CF744048FEAE818EEA256C194C80D47AF1A958F530FE817348CF383640BE9DFFB1BFB268425A52BBEACEA7CBC4BD184A6A8FC9136B25A793E2FF40A285C546F9227E6F1F47FAD180E054C980E5C611B94480C18A48A2F01128053EC3CBE69DEC6C75A7799B0E4258B1B569023F04D269FCBE229FF167EA824AA91C43E36F54D95E5C6AECAB70AF0182F800", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 18`] = ` +{ + "ct": "03FFFFFFFF0000000000000000000000000000000000000000000000001AB13417D57BB863F339C20635BD3B23E4FAC66E7DD36178E28B90200514D7B04F18AF5C6CFF2879FB516DFD5F185738E00848D1A88640498D3A0035713ADFC6860E9D99E97B5BD8E7CC259DE25828A80F349BA5A10B0AE273124D094DCA32F0E35EE635EA875E563CA7CD8570D17928826F4FF0C6F91F3153615DEECCE4B09A6A849F5DCF1078F3AA2F7846C294E98DB2A9DE614413B62A92DC17EA33835206F1C6F845234468BD25F5B72C4D1E51D360888E81180E2DF0BB6962F10AA2067AE25A5F40D981ACC841E42D094EB7D30A99F94CE64B4F2AA27ACEE5C9E8A7FBFAD5B742CA16765FE5744927600FB31661CEEDF543FCE7C3BC9F7B8A40E99D0C173EF39DA556D30217B609CF6F70669E1D45AD780205D3C4FCECCDEBE18F17FE1C08468E1DDE84E55BF9F28C57A8B40BDA2FC92BF12019C426DC2D3F068533D7316B95405D929E34687691E9B5EA09F576C1EBDDBD7AACDF949402024B8DD931EB4EECDD9EDE33A95119AEF978399E82E2D3C9B8F9A164F50639347E3A998DF69E613A0E26EC1E0C5ED7B543C0F7B749B52C99E638F9EF489F6EA64AFB2FCD7CC4A9EA0209BD546925044B3437C87F956887C23848C1293F4F1FCCC867AC225CFD2A97EAF01A99B3A2B1717CC8FBCE1D3CC3BC32197BCE5D2BD2F183DAADA30EE2252AF341170AA1BF071A0F9FBE2DBD24E6A1A026A0FACA92C74D08FE0C2F022DFCC987B371EA1598A753293BBFF041A0EB386D13F39C8A08DC59A83C2801E291B5D7CA03C588B3FFA6FF74E3B997A665", + "key": "5538573B941A9BECD69F142E256827A97D101AECCC148ABCC4EF2E7DE7E48D3E", + "pt": "03FFFFFFFF0000000000000000000000000000000000000000000000001AB13417D57BB863F339C20635BD3B23E4FAC66E7DD36178E28B902005D197C31D4C243BDE05882D841F91EE529F816DEFF4E159B6ADB3CA484B1FC25A3D0D73A00D5B30C5E334D1B603CB1B2FEEDD550BE4CB3F120377B54E3F489B952F7ED98C94B943EA633574BB5FB5A5A4A266215C73CCDD0D0E80CF615D5AFDFECAB5A576975ED04D8AC0EE99BC5FE17182F77EB83FB300AE36053DE5D5FBB269A2A9588C7ECCA55656B2A14CDEF1DBB8004E9E628EA89E52D9B944168700BA7754B3435BF2731A73F17C62DFC2AE8EC770BD17EEF91A4905E196BA5DE70929EB695D8AB337D94573FFB2A03F3482B4B70123B37F1D5C14AA98E334A55B0275DFF982A1C0C1F18E17AE144075085BADB33B6903614EC44D96BAC2249756094275C156BE180213B22C25FE81E265168DB6956A0F946D544DCA2A30F3B84C45C5A4DF294DFE2DA2CBFC9E322B7E5BC7017CC6105CDADAE13A4F7D5B47089A1EF77A677E4375215721F192FF5EF9958724A07C3E3DD5381F2C83AE62B3A8985906E54CD8F47AAB53ADF2AF6777729BD5DBA67B3D2AD460B9F312B2E038E9304EECE381972999E71CD1784EB0AA8D56A5C44560316C6C9AE02730D3642454BCAB0FC5193E24BD16863EDF0BA2FDA4B4FCAD5E22621AF535A5FD9E10A9222D25B2E1B7DDD382861994964DD78E4DE81C9C48CDDAB785C9564C31A0481E47CC789F8492EAF8D63E5D0E6156EEA274409F71743A3917EC", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 19`] = ` +{ + "ct": "03FFFFFFFF0000000000000000000000000000000000000000000000007CB22E0781C18A060871659F3A59C6F1B2FB0D9A6E31F6020774F01DC178EB6D5A19C1A405B8DAC61971D6597EC0F9702E7F47909F131F9D2A570886182F5F9E4AFA879714D878573E5DDC2DB78B0F28A95AF88472E5E5004B765D22677A9B3C2AC4256C5AAD99711FD9F2E3E10A573974E0007907F0C3A82FDFCBB12EA70F3C0108D8542CEAFE0C64B50BEF7EFB5FB1E1BF1F9EF6A01A1D9E61669A003D9FFEEB2187B4BB6ABEC799685259E357445CCA4256920E38E6AB649FFDE0683BC66CE928FFD67983042C58BBB93BF3799B51BB66C87247A20412D89AC5BB2C9E9A248515935A56D04B457A8EF41801107856FEA4A23147A46CACDF19ED7CA02FB3483CCDEE30F512312DA94EE7607B05A38A1FF40096273F460746570E2BF5458F6D8C59C0ED2CA97E063456795F98F635E0C4B5BADFAAAA831B632EB2E55C4CDF5EE1A71D0DD69EBF4C64FD5F8EA01B8889A849264AD58E868A8BD2DAE71C639B7C559E97DD30C3F9F70140AB8558EAD1D200905E61ED8FCEBFF87FF59C581EC3852A1504FF4C429303E2B9ACEEAADF5AC335F7885AE6DD8A410FB6CEFD37E88203577A31AA42474EA33A6F7365D05DBD66882CD02729A7BDE500176533BE546FB3AC0AE67CF17504B4279E4BC8841113DC89405610309FB46734C5D207877A9410BB58ED42BD46EDBD42BC525DA340068567BFCB3EE735B778C0371E13D9E5C2FA69FC4C9C5EE1E3930FDBEE185BCB2B25F6C3EC0AFC43E34D8CC95C558F30796CF2327D85E6F8E11BC3FBD742742F8AEDA83C4FBBED249E48EF379B15F166D73C823B4EE2D8C81C0611B45FF168EB8020", + "key": "C9564C31A0481E47CC789F8492EAF8D63E5D0E6156EEA274409F71743A3917EC", + "pt": "03FFFFFFFF0000000000000000000000000000000000000000000000001AB13417D57BB863F339C20635BD3B23E4FAC66E7DD36178E28B90200514D7B04F18AF5C6CFF2879FB516DFD5F185738E00848D1A88640498D3A0035713ADFC6860E9D99E97B5BD8E7CC259DE25828A80F349BA5A10B0AE273124D094DCA32F0E35EE635EA875E563CA7CD8570D17928826F4FF0C6F91F3153615DEECCE4B09A6A849F5DCF1078F3AA2F7846C294E98DB2A9DE614413B62A92DC17EA33835206F1C6F845234468BD25F5B72C4D1E51D360888E81180E2DF0BB6962F10AA2067AE25A5F40D981ACC841E42D094EB7D30A99F94CE64B4F2AA27ACEE5C9E8A7FBFAD5B742CA16765FE5744927600FB31661CEEDF543FCE7C3BC9F7B8A40E99D0C173EF39DA556D30217B609CF6F70669E1D45AD780205D3C4FCECCDEBE18F17FE1C08468E1DDE84E55BF9F28C57A8B40BDA2FC92BF12019C426DC2D3F068533D7316B95405D929E34687691E9B5EA09F576C1EBDDBD7AACDF949402024B8DD931EB4EECDD9EDE33A95119AEF978399E82E2D3C9B8F9A164F50639347E3A998DF69E613A0E26EC1E0C5ED7B543C0F7B749B52C99E638F9EF489F6EA64AFB2FCD7CC4A9EA0209BD546925044B3437C87F956887C23848C1293F4F1FCCC867AC225CFD2A97EAF01A99B3A2B1717CC8FBCE1D3CC3BC32197BCE5D2BD2F183DAADA30EE2252AF341170AA1BF071A0F9FBE2DBD24E6A1A026A0FACA92C74D08FE0C2F022DFCC987B371EA1598A753293BBFF041A0EB386D13F39C8A08DC59A83C2801E291B5D7CA03C588B3FFA6FF74E3B997A665", +} +`; + +exports[`Twofish > Cipher must match reference test vectors cascade 20`] = ` +{ + "ct": "03FFFFFFFF0000000000000000000000000000000000000000000000007CB22E0781C18A060871659F3A59C6F1B2FB0D9A6E31F6020774F01DC11EE8774A4D7B9660439261807E32A4AC96F8BBDA6CA507E5F6E0FD179364BAC53A5EF0B293C065AB3A58EC9A7C0483FF457827E86D6DDBA6428808F81F48A23FB66F262439864FD5740888AF8C21C461AC395A5533EE96E5C8A7B804F57B18E26E95FF58B8F2930BC980330224D0C16F0DD0FE687E16426FDCA48BC1832525A65613B9E2C27D3244A424B430D43DEFDF04529D35F0E53E39345BDA14AC432D9021211318B0D100033F99C92D15F02E7CD35189A870FEB91C7DF28DC9F85C7B3A95E88413B591F2B2C914D148333A505A32E7D26AA84F1FF706388CADDDD41D7E7F1199AC8D4824551CF6FED140440E4C35EADD6D4F4339377F24607A1EBC94AA47B11E19FA01DE16B6392E3B5D1A620FAC6C985097565D510019FEA954E0942DC8C010115530968B531BB96CCD1DB09AF0F41886C3950DE8318507680BA5EF73E9A92105DA975614D195413A0AB9F8A7837329F73F7C38F3901E6754EE4F36CF05B1CF69FBF4AFEA5A7D511FD8D8BD9A0C0325ADBB63615F1CC1CAA9775DC82CA3AD098EDA6D351F72424C14321B76A644D308EA33E2E5F7C6AD61060F40CD79AAFF800FF5F55B774B20F3758669E08A1EFD34E6A8D8929A806FCA60D7A434C9FC091C37B301F5ABD0028A42C38D53430AFAE16EF49AD7ABD383291D1DC7892B7BB0E46A5600D05CA445CECDC683A3FA4A354EEE8423771644995B611C969558A54CA7DD1D7673ED5069DCE01FEC28F132716CB13390D2A8740EBC8690AA8E0C21328B04183EB03CAD18A5B079E7DDE043BC6F6B5471AD88172DCDF7792D13B631C341623214F97961DBCEABA33DE97A9EE67C", + "key": "C9564C31A0481E47CC789F8492EAF8D63E5D0E6156EEA274409F71743A3917EC", + "pt": "03FFFFFFFF0000000000000000000000000000000000000000000000007CB22E0781C18A060871659F3A59C6F1B2FB0D9A6E31F6020774F01DC178EB6D5A19C1A405B8DAC61971D6597EC0F9702E7F47909F131F9D2A570886182F5F9E4AFA879714D878573E5DDC2DB78B0F28A95AF88472E5E5004B765D22677A9B3C2AC4256C5AAD99711FD9F2E3E10A573974E0007907F0C3A82FDFCBB12EA70F3C0108D8542CEAFE0C64B50BEF7EFB5FB1E1BF1F9EF6A01A1D9E61669A003D9FFEEB2187B4BB6ABEC799685259E357445CCA4256920E38E6AB649FFDE0683BC66CE928FFD67983042C58BBB93BF3799B51BB66C87247A20412D89AC5BB2C9E9A248515935A56D04B457A8EF41801107856FEA4A23147A46CACDF19ED7CA02FB3483CCDEE30F512312DA94EE7607B05A38A1FF40096273F460746570E2BF5458F6D8C59C0ED2CA97E063456795F98F635E0C4B5BADFAAAA831B632EB2E55C4CDF5EE1A71D0DD69EBF4C64FD5F8EA01B8889A849264AD58E868A8BD2DAE71C639B7C559E97DD30C3F9F70140AB8558EAD1D200905E61ED8FCEBFF87FF59C581EC3852A1504FF4C429303E2B9ACEEAADF5AC335F7885AE6DD8A410FB6CEFD37E88203577A31AA42474EA33A6F7365D05DBD66882CD02729A7BDE500176533BE546FB3AC0AE67CF17504B4279E4BC8841113DC89405610309FB46734C5D207877A9410BB58ED42BD46EDBD42BC525DA340068567BFCB3EE735B778C0371E13D9E5C2FA69FC4C9C5EE1E3930FDBEE185BCB2B25F6C3EC0AFC43E34D8CC95C558F30796CF2327D85E6F8E11BC3FBD742742F8AEDA83C4FBBED249E48EF379B15F166D73C823B4EE2D8C81C0611B45FF168EB8020", +} +`; diff --git a/src/core/encryption/__tests__/__snapshots__/encryption.consistency.test.ts.snap b/src/core/encryption/__tests__/__snapshots__/encryption.consistency.test.ts.snap deleted file mode 100644 index 88d450b14..000000000 --- a/src/core/encryption/__tests__/__snapshots__/encryption.consistency.test.ts.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`composed processors returns idempotent result 1`] = `-466581813`; diff --git a/src/core/encryption/__tests__/__snapshots__/random.test.ts.snap b/src/core/encryption/__tests__/__snapshots__/random.test.ts.snap new file mode 100644 index 000000000..60062eb57 --- /dev/null +++ b/src/core/encryption/__tests__/__snapshots__/random.test.ts.snap @@ -0,0 +1,37 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Fake random > mulberry32 sequence > Seed 0 1`] = ` +[ + 0.26642920868471265, + 0.0003297457005828619, + 0.2232720274478197, + 0.1462021479383111, + 0.46732782293111086, + 0.5450490827206522, + 0.6152513844426721, + 0.6489853798411787, + 0.45600721263326705, + 0.581218967679888, +] +`; + +exports[`Fake random > mulberry32 sequence > Seed 1 1`] = ` +[ + 0.6270739405881613, + 0.002735721180215478, + 0.5274470399599522, + 0.9810509674716741, + 0.9683778982143849, + 0.281103502959013, + 0.6128388606011868, + 0.7207431411370635, + 0.425796952098608, + 0.9948229456786066, +] +`; + +exports[`Fake random > random bytes sequence > Seed 0 1`] = `"44003925778b9da67494"`; + +exports[`Fake random > random bytes sequence > Seed 1 1`] = `"a00087fbf7479cb86dfe"`; + +exports[`Fake random > random bytes sequence > Seed 100 1`] = `"345088e38daf03853a80"`; diff --git a/src/core/encryption/__tests__/ciphers.spec.test.ts b/src/core/encryption/__tests__/ciphers.spec.test.ts new file mode 100644 index 000000000..a5734abf9 --- /dev/null +++ b/src/core/encryption/__tests__/ciphers.spec.test.ts @@ -0,0 +1,98 @@ +import { webcrypto } from 'node:crypto'; + +import { getRandomBytes } from '../utils/random'; +import { ciphers } from './ciphers'; +import { createFakeRandomBytesGenerator } from './random'; +import { fromHex, toHex } from './utils'; + +vi.stubGlobal('crypto', webcrypto); +vi.stubGlobal('self', globalThis); + +ciphers.map((cipher) => + describe(cipher.name, () => { + test('Encrypted text may be decrypted', async () => { + const getRandomBytes = createFakeRandomBytesGenerator(0); + const key = getRandomBytes(32); + const codec = await cipher.create(key, getRandomBytes); + + const originalData = getRandomBytes(1024 * 1024); + + const ct = await codec.encrypt(originalData.buffer.slice()); + await expect( + codec.decrypt(ct).then((buffer) => toHex(new Uint8Array(buffer))), + ).resolves.toBe(toHex(new Uint8Array(originalData.buffer))); + }); + + test('Encrypted text may be decrypted by another instance with the equal key', async () => { + const pt = getRandomBytes(1000); + const key = getRandomBytes(32); + + const cipher1 = await cipher.create(key.slice(), getRandomBytes); + const ct = await cipher1.encrypt(pt.slice().buffer); + + const cipher2 = await cipher.create(key.slice(), getRandomBytes); + await expect( + cipher2.decrypt(ct).then((buffer) => Buffer.from(buffer).toString('hex')), + ).resolves.toStrictEqual(Buffer.from(pt.buffer).toString('hex')); + }); + + test('The encryption of same plaintext must return different cipher texts', async () => { + const key = getRandomBytes(32); + const codec = await cipher.create(key, getRandomBytes); + const originalData = getRandomBytes(1024).buffer; + + // All cipher text is unique + const cipherTexts = new Set(); + for (let i = 0; i < 100; i++) { + const ct = await codec.encrypt(originalData); + const ctHex = toHex(new Uint8Array(ct)); + expect(cipherTexts.has(ctHex), 'Each cipher text are unique').toBe(false); + + cipherTexts.add(ctHex); + expect(cipherTexts.has(ctHex), 'Ensure the cipher text is recorded').toBe( + true, + ); + } + + // All cipher texts must be decrypted to a buffer equal to original + for (const ct of cipherTexts) { + await expect(codec.decrypt(fromHex(ct).buffer)).resolves.toStrictEqual( + originalData, + ); + } + }); + + test('Cipher text must not leak any patterns', async () => { + const getSeededRandomBytes = createFakeRandomBytesGenerator(0); + const key = getSeededRandomBytes(32); + const codec = await cipher.create(key, getSeededRandomBytes); + + // 1. highly repetitive input + const input = new Uint8Array(10_000).fill(0x41); + + const ct = await codec + .encrypt(input.buffer) + .then((buffer) => new Uint8Array(buffer)); + + // 2. sliding window duplicate detection + const window = 8; // small, mode-agnostic + const seen = new Map(); + + onTestFailed(() => { + console.log('Debug information'); + console.log({ key: Buffer.from(key).toString('hex'), window, seen }); + console.log('Cipher text', Buffer.from(ct).toString('hex')); + }); + + for (let i = 0; i <= ct.length - window; i++) { + const slice = ct.subarray(i, i + window); + const key = Buffer.from(slice).toString('hex'); + + expect(seen.has(key), key).toBe(false); + seen.set(key, i); + } + + expect(true).toBe(true); + }); + }), +); diff --git a/src/core/encryption/__tests__/ciphers.ts b/src/core/encryption/__tests__/ciphers.ts new file mode 100644 index 000000000..8107ba844 --- /dev/null +++ b/src/core/encryption/__tests__/ciphers.ts @@ -0,0 +1,68 @@ +import { ENCRYPTION_ALGORITHM } from '@core/features/encryption'; +import { formatAlgorithms } from '@core/features/encryption/utils'; + +import { AESCipher } from '../ciphers/AES'; +import { SerpentCipher } from '../ciphers/Serpent'; +import { TwofishCTRCipher } from '../ciphers/Twofish'; +import { XChaCha20Cipher } from '../ciphers/XChaCha20'; +import { BufferIntegrityProcessor } from '../processors/BufferIntegrityProcessor'; +import { PipelineProcessor } from '../processors/PipelineProcessor'; +import { HKDFDerivedKeys } from '../utils/HKDFDerivedKeys'; +import { IEncryptionProcessor, RandomBytesGenerator } from '..'; + +export const ciphers: { + name: string; + create( + key: Uint8Array, + randomBytes: RandomBytesGenerator, + ): Promise; +}[] = [ + { + name: ENCRYPTION_ALGORITHM.AES, + async create(key, randomBytes) { + return new AESCipher(key, randomBytes); + }, + }, + { + name: ENCRYPTION_ALGORITHM.TWOFISH, + async create(key, randomBytes) { + return new TwofishCTRCipher(key, randomBytes); + }, + }, + { + name: ENCRYPTION_ALGORITHM.SERPENT, + async create(key, randomBytes) { + return new SerpentCipher(key, randomBytes); + }, + }, + { + name: ENCRYPTION_ALGORITHM.XChaCha20, + async create(key, randomBytes) { + return new XChaCha20Cipher(key, randomBytes); + }, + }, + { + name: formatAlgorithms([ + ENCRYPTION_ALGORITHM.AES, + ENCRYPTION_ALGORITHM.TWOFISH, + ENCRYPTION_ALGORITHM.SERPENT, + ]), + async create(key, randomBytes) { + const hkdf = new HKDFDerivedKeys(key, new Uint8Array(32)); + + const [hmac, aes, twofish, serpent] = await Promise.all([ + hkdf.deriveBytes(32, 'hmac'), + hkdf.deriveBytes(32, 'aes'), + hkdf.deriveBytes(32, 'twofish'), + hkdf.deriveBytes(32, 'serpent'), + ]); + + return new PipelineProcessor([ + new BufferIntegrityProcessor(hmac), + new AESCipher(aes, randomBytes), + new TwofishCTRCipher(twofish, randomBytes), + new SerpentCipher(serpent, randomBytes), + ]); + }, + }, +]; diff --git a/src/core/encryption/__tests__/ciphers.vectors.test.ts b/src/core/encryption/__tests__/ciphers.vectors.test.ts new file mode 100644 index 000000000..2a53f60c3 --- /dev/null +++ b/src/core/encryption/__tests__/ciphers.vectors.test.ts @@ -0,0 +1,56 @@ +import { webcrypto } from 'node:crypto'; + +import { ENCRYPTION_ALGORITHM } from '@core/features/encryption'; + +import { ciphers } from './ciphers'; +import { fromHex, toHex } from './utils'; + +vi.stubGlobal('crypto', webcrypto); +vi.stubGlobal('self', globalThis); + +const targetCiphers = new Set([ + ENCRYPTION_ALGORITHM.AES, + ENCRYPTION_ALGORITHM.TWOFISH, + ENCRYPTION_ALGORITHM.SERPENT, +]); + +ciphers + .filter((cipher) => targetCiphers.has(cipher.name)) + .map((cipher) => + describe(cipher.name, () => { + test('Cipher must match reference test vectors cascade', async () => { + // We disable any random factor for that test + const randomBytesMock = (len: number) => new Uint8Array(len); + + const keySize = 32; + let key = fromHex( + '0000000000000000000000000000000000000000000000000000000000000000', + ); + let pt = fromHex('00000000000000000000000000000000').buffer; + + for (let i = 1; i <= 10; i++) { + const cipher1 = await cipher.create(key, randomBytesMock); + const ct1 = await cipher1.encrypt(pt); + expect({ + key: toHex(key), + pt: toHex(new Uint8Array(pt)), + ct: toHex(new Uint8Array(ct1)), + }).toMatchSnapshot(); + + pt = ct1; + + const cipher2 = await cipher.create(key, randomBytesMock); + const ct2 = await cipher2.encrypt(pt); + expect({ + key: toHex(key), + pt: toHex(new Uint8Array(pt)), + ct: toHex(new Uint8Array(ct2)), + }).toMatchSnapshot(); + + // We take only last N bytes to use it as a valid key + key = new Uint8Array(ct1).slice(-keySize); + pt = ct2; + } + }); + }), + ); diff --git a/src/core/encryption/__tests__/encryption.consistency.test.ts b/src/core/encryption/__tests__/encryption.consistency.test.ts deleted file mode 100644 index 7e1cdb114..000000000 --- a/src/core/encryption/__tests__/encryption.consistency.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import crc32 from 'crc/calculators/crc32'; -import { webcrypto } from 'crypto'; - -import { AESGCMCipher } from '../ciphers/AES'; -import { TwofishCTRCipher } from '../ciphers/Twofish'; -import { BufferIntegrityProcessor } from '../processors/BufferIntegrityProcessor'; -import { BufferSizeObfuscationProcessor } from '../processors/BufferSizeObfuscationProcessor'; -import { PipelineProcessor } from '../processors/PipelineProcessor'; -import { getDerivedKeysManager, getMasterKey } from '../utils/keys'; - -// Returns always the same output, to reproduce encryption output -// Implementation generates a long sequence with no repeats, for transparent fingerprint analyzing -const getRandomBytesMock = (length = 16) => - new Uint8Array(length).map( - (_, idx) => idx + Math.max(0, idx + ((length + idx) % 255)), - ).buffer; - -const getKeys = async (parameters: { password: string; salt: string }) => { - const password = new TextEncoder().encode(parameters.password); - const salt = new TextEncoder().encode(parameters.salt); - - const derivedKeys = await getMasterKey(password.buffer, salt.buffer).then( - (masterKey) => getDerivedKeysManager(masterKey, salt), - ); - - const aes = await derivedKeys.getDerivedKey('aes-gcm-cipher', { - name: 'AES-GCM', - length: 256, - }); - - const twofish = await derivedKeys - .getDerivedBytes('twofish-ctr-cipher', 256) - .then((buffer) => new Uint8Array(buffer)); - - return { aes, twofish }; -}; - -// Mock for `crypto` for jest context -beforeEach(() => { - Object.defineProperties(global, { - crypto: { value: webcrypto, writable: true }, - self: { value: global, writable: false }, - }); -}); - -test('composed processors returns idempotent result', async () => { - const keys = await getKeys({ - password: 'SuperSecretPassword', - salt: 'salt bytes', - }); - - const cipher = new PipelineProcessor([ - new BufferIntegrityProcessor(), - new BufferSizeObfuscationProcessor(getRandomBytesMock), - new TwofishCTRCipher(keys.twofish, getRandomBytesMock), - new AESGCMCipher(keys.aes, getRandomBytesMock), - ]); - - const textSample = 'Hello world! This is encryption example text'; - const encryptedBytes = await cipher.encrypt( - new TextEncoder().encode(textSample).buffer, - ); - expect(crc32(new Uint8Array(encryptedBytes))).toMatchSnapshot(); - - const decryptedBytes = await cipher.decrypt(encryptedBytes); - expect(new TextDecoder().decode(decryptedBytes)).toBe(textSample); - - // Encryption for the same text input must be equal, because mocked random generator generates identical sequences - const encryptedBytes2 = await cipher.encrypt( - new TextEncoder().encode(textSample).buffer, - ); - expect(new Uint32Array(encryptedBytes)).toStrictEqual( - new Uint32Array(encryptedBytes2), - ); - - // Encryption for 2 different texts must not be equal - const encryptedBytes3 = await cipher.encrypt( - new TextEncoder().encode('Another text').buffer, - ); - expect(new Uint32Array(encryptedBytes)).not.toEqual(new Uint32Array(encryptedBytes3)); - - // Random bytes decryption must throw exception, because integrity check error - const randomBytes = getRandomBytesMock(800); - await expect(cipher.decrypt(randomBytes)).rejects.toThrow(); -}); diff --git a/src/core/encryption/__tests__/random.test.ts b/src/core/encryption/__tests__/random.test.ts new file mode 100644 index 000000000..af999d19c --- /dev/null +++ b/src/core/encryption/__tests__/random.test.ts @@ -0,0 +1,39 @@ +import { createFakeRandomBytesGenerator, mulberry32 } from './random'; + +describe('Fake random', () => { + test('mulberry32 sequence', () => { + const seq1 = mulberry32(0); + expect( + Array(10) + .values() + .map(() => seq1()) + .toArray(), + ).toMatchSnapshot('Seed 0'); + + const seq2 = mulberry32(1); + expect( + Array(10) + .values() + .map(() => seq2()) + .toArray(), + ).toMatchSnapshot('Seed 1'); + }); + + test('random bytes sequence', () => { + expect( + Buffer.from(createFakeRandomBytesGenerator(0)(10)).toString('hex'), + ).toMatchSnapshot('Seed 0'); + expect( + Buffer.from(createFakeRandomBytesGenerator(1)(10)).toString('hex'), + ).toMatchSnapshot('Seed 1'); + expect( + Buffer.from(createFakeRandomBytesGenerator(100)(10)).toString('hex'), + ).toMatchSnapshot('Seed 100'); + }); + + test('fake random bytes can reach the bounds', () => { + const sample = createFakeRandomBytesGenerator(1)(4096); + expect(Math.max(...sample)).toBe(255); + expect(Math.min(...sample)).toBe(0); + }); +}); diff --git a/src/core/encryption/__tests__/random.ts b/src/core/encryption/__tests__/random.ts new file mode 100644 index 000000000..21c171db6 --- /dev/null +++ b/src/core/encryption/__tests__/random.ts @@ -0,0 +1,16 @@ +/* eslint-disable no-bitwise */ +export function mulberry32(seed: number): () => number { + let t = seed >>> 0; + return () => { + t += 0x6d2b79f5; + let r = Math.imul(t ^ (t >>> 15), 1 | t); + r ^= r + Math.imul(r ^ (r >>> 7), 61 | r); + return ((r ^ (r >>> 14)) >>> 0) / 4294967296; + }; +} + +export const createFakeRandomBytesGenerator = (seed: number) => { + const getNextNumber = mulberry32(seed); + return (length: number) => + new Uint8Array(length).map(() => Math.floor(getNextNumber() * 256)); +}; diff --git a/src/core/encryption/__tests__/utils.ts b/src/core/encryption/__tests__/utils.ts new file mode 100644 index 000000000..aa81907d8 --- /dev/null +++ b/src/core/encryption/__tests__/utils.ts @@ -0,0 +1,6 @@ +export function fromHex(str: string) { + return new Uint8Array(Buffer.from(str, 'hex')); +} +export function toHex(buf: Uint8Array) { + return Buffer.from(buf).toString('hex').toUpperCase(); +} diff --git a/src/core/encryption/cipherModes/CTRCipherMode.ts b/src/core/encryption/cipherModes/CTRCipherMode.ts index 9fa4e38e0..27ec73d3d 100644 --- a/src/core/encryption/cipherModes/CTRCipherMode.ts +++ b/src/core/encryption/cipherModes/CTRCipherMode.ts @@ -1,65 +1,78 @@ -import { xor } from '../utils/xor'; - /** * Stream cipher with counter implementation * Read more on https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_(CTR) * RFC: https://datatracker.ietf.org/doc/html/rfc3686 */ export class CTRCipherMode { - private readonly encryptBuffer; - private readonly blockSize; constructor( - encryptBuffer: (buffer: ArrayBuffer) => Promise, - blockSize = 16, - ) { - this.encryptBuffer = encryptBuffer; - this.blockSize = blockSize; + private readonly encryptBuffer: (buffer: Uint8Array) => Uint8Array, + private readonly xor: (a: Uint8Array, b: Uint8Array) => Uint8Array, + private readonly blockSize = 16, + ) {} + + public static getEncryptionLimits(blockSize: number) { + // We use 4 bytes counter that is 32 bits + const counterStates = 2 ** 32; + return { + maxBlocks: counterStates, + maxBytes: counterStates * blockSize - 1, + }; } - public async encrypt(buffer: ArrayBuffer, iv: ArrayBuffer) { + public async encrypt(buffer: Uint8Array, iv: Uint8Array) { if (buffer.byteLength % this.blockSize !== 0) - throw new TypeError(`Buffer size is not multiple to ${this.blockSize}`); - if (iv.byteLength % this.blockSize !== 0) throw new TypeError( - `Initialization vector size is not multiple to ${this.blockSize} bytes`, + `Buffer size ${buffer.byteLength} bytes is not multiple to ${this.blockSize}. Did you mean to add padding?`, ); - const counterBuffer = new Uint8Array(this.blockSize); - const counterView = new DataView(counterBuffer.buffer); - const ivView = new Uint8Array(iv); + const limits = CTRCipherMode.getEncryptionLimits(this.blockSize); - const out = new Uint8Array(buffer.byteLength); - for (let offset = 0; offset < buffer.byteLength; offset += this.blockSize) { - // XOR a Nonce and Counter - const ivOffset = offset % ivView.byteLength; - const uniqueSequence = xor( - ivView.slice(ivOffset, ivOffset + this.blockSize), - counterBuffer, + // (2**32 * 16) / (1024 ** 3) = 64Gb is a total size may be encrypted with block size = 16 + // Next blocks would repeat the sequence, so we cannot allow to continue + if (buffer.byteLength > limits.maxBytes) + throw new RangeError( + `Buffer size ${buffer.byteLength} is out of bytes length that may be encrypted safely via 4bytes counter (${limits.maxBytes})`, + ); + + if (iv.byteLength + 4 < this.blockSize) + throw new TypeError( + `Initialization vector have only ${iv.byteLength} bytes. It is too small and must be at least ${this.blockSize - 4}`, ); + const out = new Uint8Array(buffer.byteLength); + + // Layout is `nonce || counter` + const input = new Uint8Array(this.blockSize); + input.set(iv.slice(0, this.blockSize - 4)); + + const counterView = new DataView(input.buffer, this.blockSize - 4); + + const blocksCount = Math.ceil(buffer.byteLength / this.blockSize); + for (let i = 0; i < blocksCount; ) { + const offset = i * this.blockSize; + // Encrypt unique sequence - const encryptedSequence = await this.encryptBuffer(uniqueSequence.buffer); + const encryptedSequence = this.encryptBuffer(input); + // TODO: provide an efficient xor function as dependency // XOR unique sequence and block data, // a data is a plain text for encryption and cipher text for decryption - const dataSlice = buffer.slice(offset, offset + this.blockSize); - const dataBlock = xor( - new Uint8Array(encryptedSequence), - new Uint8Array(dataSlice), + const dataBlock = this.xor( + encryptedSequence, + buffer.subarray(offset, offset + this.blockSize), ); // Write block to a out buffer out.set(dataBlock, offset); // Increment counter - const nextCounter = counterView.getUint32(0) + 1; - counterView.setUint32(0, nextCounter); + counterView.setUint32(0, ++i); } - return out.buffer; + return out; } - public async decrypt(buffer: ArrayBuffer, iv: ArrayBuffer) { + public async decrypt(buffer: Uint8Array, iv: Uint8Array) { // CTR mode do the same operation for decryption as for encryption, // but instead of plain text it handles cipher text return this.encrypt(buffer, iv); diff --git a/src/core/encryption/cipherModes/CTRMode.test.ts b/src/core/encryption/cipherModes/CTRMode.test.ts new file mode 100644 index 000000000..2097832b7 --- /dev/null +++ b/src/core/encryption/cipherModes/CTRMode.test.ts @@ -0,0 +1,140 @@ +/* eslint-disable @cspell/spellchecker */ +/* eslint-disable no-bitwise */ +import { describe, expect, it } from 'vitest'; + +import { xor16 } from '../utils/xor'; +import { CTRCipherMode } from './CTRCipherMode'; + +const makeXorEncrypt = + (key: number) => + (buffer: Uint8Array): Uint8Array => + new Uint8Array(buffer.map((b) => b ^ key)); + +// We preallocate the RAM and re-use it, +// since only one XOR buffer is used at once, +// and we synchronously read the result before run next XOR +const xorBuffer = new Uint8Array(16); +const xor = (a: Uint8Array, b: Uint8Array) => xor16(xorBuffer, a, b); + +describe('CTRCipherMode', () => { + it('output length equals input length', async () => { + const ctr = new CTRCipherMode(makeXorEncrypt(0xab), xor); + const iv = new Uint8Array(16).fill(0x00); + const plaintext = new Uint8Array(32).fill(0x42); + const ciphertext = await ctr.encrypt(plaintext, iv); + expect(ciphertext.length).toBe(32); + }); + + it('encrypt is its own inverse (decrypt = encrypt)', async () => { + const ctr = new CTRCipherMode(makeXorEncrypt(0xab), xor); + const iv = new Uint8Array(16).fill(0x00); + const plaintext = new Uint8Array(32).fill(0x42); + const ciphertext = await ctr.encrypt(plaintext, iv); + const recovered = await ctr.encrypt(ciphertext, iv); + expect(recovered).toEqual(plaintext); + }); + + it('different IVs produce different ciphertexts', async () => { + const ctr = new CTRCipherMode(makeXorEncrypt(0xab), xor); + const plaintext = new Uint8Array(16).fill(0x42); + const ct1 = await ctr.encrypt(plaintext, new Uint8Array(16).fill(0x00)); + const ct2 = await ctr.encrypt(plaintext, new Uint8Array(16).fill(0x01)); + expect(ct1).not.toEqual(ct2); + }); + + it('does not mutate the IV', async () => { + const ctr = new CTRCipherMode(makeXorEncrypt(0xab), xor); + const iv = new Uint8Array(16).fill(0x00); + const ivCopy = new Uint8Array(iv); + await ctr.encrypt(new Uint8Array(48).fill(0x42), iv); + expect(iv).toEqual(ivCopy); + }); + + it('does not mutate the plaintext', async () => { + const ctr = new CTRCipherMode(makeXorEncrypt(0xab), xor); + const iv = new Uint8Array(16).fill(0x00); + const plaintext = new Uint8Array(48).fill(0x42); + const snapshot = new Uint8Array(plaintext); + await ctr.encrypt(plaintext, iv); + expect(plaintext).toEqual(snapshot); + }); + + it('handles empty input', async () => { + const ctr = new CTRCipherMode(makeXorEncrypt(0xab), xor); + const iv = new Uint8Array(16).fill(0x00); + const result = await ctr.encrypt(new Uint8Array(0), iv); + expect(result.length).toBe(0); + }); +}); + +// identity encrypt + zero plaintext → ciphertext = raw counter blocks +const identityEncrypt = (buffer: Uint8Array): Uint8Array => new Uint8Array(buffer); + +// const makeXorEncrypt = (key: number) => +// async (buffer: Uint8Array): Promise => +// new Uint8Array(buffer.map((b) => b ^ key)); + +describe('CTRCipherMode – cryptographic correctness', () => { + it('ciphertext equals plaintext XOR keystream', async () => { + const ctr = new CTRCipherMode(makeXorEncrypt(0xab), xor); + const iv = new Uint8Array(16).fill(0x00); + + // encrypting zeros gives raw keystream (0 XOR KS = KS) + const keystream = await ctr.encrypt(new Uint8Array(32).fill(0x00), iv); + const plaintext = new Uint8Array(32).map((_, i) => i); + const ciphertext = await ctr.encrypt(plaintext, iv); + + const expected = new Uint8Array(plaintext.map((b, i) => b ^ keystream[i])); + expect(ciphertext).toEqual(expected); + }); + + it('each block is encrypted with a unique counter', async () => { + const ctr = new CTRCipherMode(identityEncrypt, xor); + const iv = new Uint8Array(16).fill(0x00); + + const ciphertext = await ctr.encrypt(new Uint8Array(48).fill(0x00), iv); + const block0 = ciphertext.slice(0, 16); + const block1 = ciphertext.slice(16, 32); + const block2 = ciphertext.slice(32, 48); + + expect(block0).not.toEqual(block1); + expect(block1).not.toEqual(block2); + }); + + it('counter increments consistently between blocks', async () => { + const ctr = new CTRCipherMode(identityEncrypt, xor); + const iv = new Uint8Array(16).fill(0x00); + + const ciphertext = await ctr.encrypt(new Uint8Array(48).fill(0x00), iv); + const block0 = Array.from(ciphertext.slice(0, 16)); + const block1 = Array.from(ciphertext.slice(16, 32)); + const block2 = Array.from(ciphertext.slice(32, 48)); + + const diff01 = block1.map((b, i) => b - block0[i]); + const diff12 = block2.map((b, i) => b - block1[i]); + + // same increment pattern each time, and it must actually change + expect(diff01).toEqual(diff12); + expect(diff01.some((d) => d !== 0)).toBe(true); + }); + + it('partial last block uses only the required keystream bytes', async () => { + const ctr = new CTRCipherMode(identityEncrypt, xor); + const iv = new Uint8Array(16).fill(0x00); + await expect(ctr.encrypt(new Uint8Array(19).fill(0x00), iv)).rejects.toThrow( + 'Did you mean to add padding?', + ); + }); + + it('keystream is consistent regardless of message length', async () => { + const ctr = new CTRCipherMode(makeXorEncrypt(0x42), xor); + const iv = new Uint8Array(16).fill(0x00); + const plaintext = new Uint8Array(48).fill(0xaa); + + const full = await ctr.encrypt(plaintext, iv); + const firstBlock = await ctr.encrypt(plaintext.slice(0, 16), iv); + + // first block of a long message must equal a standalone single-block encryption + expect(full.slice(0, 16)).toEqual(firstBlock); + }); +}); diff --git a/src/core/encryption/ciphers/AES/AES.spec.test.ts b/src/core/encryption/ciphers/AES/AES.spec.test.ts new file mode 100644 index 000000000..1fe705d49 --- /dev/null +++ b/src/core/encryption/ciphers/AES/AES.spec.test.ts @@ -0,0 +1,57 @@ +/* eslint-disable no-bitwise */ +function hex(hex: string): Uint8Array { + return new Uint8Array(hex.match(/.{2}/g)!.map((b) => parseInt(b, 16))); +} + +function incrementCounter(counter: Uint8Array, n: number): Uint8Array { + const c = new Uint8Array(counter); + let carry = n; + for (let i = 15; i >= 12; i--) { + // last 4 bytes + const sum = c[i] + carry; + c[i] = sum & 0xff; + carry = sum >> 8; + } + return c; +} + +// Source: https://nvlpubs.nist.gov/nistpubs/legacy/sp/nistspecialpublication800-38a.pdf + +describe('AES-128-CTR NIST SP 800-38A multi-block', () => { + const key = hex('2b7e151628aed2a6abf7158809cf4f3c'); + const iv = hex('f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff'); + const plaintextBlocks = [ + '6bc1bee22e409f96e93d7e117393172a', + 'ae2d8a571e03ac9c9eb76fac45af8e51', + '30c81c46a35ce411e5fbc1191a0a52ef', + 'f69f2445df4f9b17ad2b417be66c3710', + ].map(hex); + const expectedCiphertextBlocks = [ + '874d6191b620e3261bef6864990db6ce', + '9806f66b7970fdff8617187bb9fffdff', + '5ae4df3edbd5d35e5b4f09020db03eab', + '1e031dda2fbe03d1792170a0f3009cee', + ].map(hex); + + it('matches all 4 blocks with proper counter increment', async () => { + const cryptoKey = await crypto.subtle.importKey( + 'raw', + key, + { name: 'AES-CTR' }, + false, + ['encrypt'], + ); + + for (let i = 0; i < plaintextBlocks.length; i++) { + const counter = incrementCounter(iv, i); + const result = new Uint8Array( + await crypto.subtle.encrypt( + { name: 'AES-CTR', counter, length: 128 }, + cryptoKey, + plaintextBlocks[i], + ), + ); + expect(result).toEqual(expectedCiphertextBlocks[i]); + } + }); +}); diff --git a/src/core/encryption/ciphers/AES/index.ts b/src/core/encryption/ciphers/AES/index.ts index cbfba5604..677b4274f 100644 --- a/src/core/encryption/ciphers/AES/index.ts +++ b/src/core/encryption/ciphers/AES/index.ts @@ -1,75 +1,79 @@ -/* eslint-disable @cspell/spellchecker */ -import { IntegrityError } from '../../processors/BufferIntegrityProcessor'; +import { bytes, struct } from '@core/encryption/utils/bytes/binstruct'; +import { HKDFDerivedKeys } from '@core/encryption/utils/HKDFDerivedKeys'; + import { joinBuffers } from '../../utils/buffers'; import { IEncryptionProcessor, RandomBytesGenerator } from '../..'; +const AESHeader = struct({ + iv: bytes(32), +}); + /** - * AES-GCM cipher - * MDN: https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams + * AES-CTR cipher + * MDN: https://developer.mozilla.org/en-US/docs/Web/API/AesCtrParams * Algorithm recommendations: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf */ -export class AESGCMCipher implements IEncryptionProcessor { - private readonly ivSize = 96; - - private readonly key; - private readonly randomBytesGenerator: RandomBytesGenerator; - constructor(key: CryptoKey, randomBytesGenerator: RandomBytesGenerator) { - this.key = key; - this.randomBytesGenerator = randomBytesGenerator; - } +export class AESCipher implements IEncryptionProcessor { + constructor( + private readonly key: Uint8Array, + private readonly randomBytesGenerator: RandomBytesGenerator, + ) {} public async encrypt(buffer: ArrayBuffer) { - const iv = this.randomBytesGenerator(this.ivSize); - const encryptedBuffer = await self.crypto.subtle.encrypt( - { - name: 'AES-GCM', + const iv = this.randomBytesGenerator(32); - // Don't re-use initialization vectors! - // Always generate a new iv every time your encrypt! - // Recommended to use 96 bytes length - iv, + const hkdf = new HKDFDerivedKeys(this.key, iv); + const [key, messageIV] = await Promise.all([ + hkdf + .deriveBytes(32, 'msg') + .then((key) => + crypto.subtle.importKey('raw', key, 'AES-CTR', false, [ + 'encrypt', + 'decrypt', + ]), + ), + hkdf.deriveBytes(16, 'iv'), + ]); - // Tag length (optional) - // can be 32, 64, 96, 104, 112, 120 or 128 (default) - tagLength: 128, + const encryptedBuffer = await self.crypto.subtle.encrypt( + { + name: 'AES-CTR', + counter: messageIV, + length: 64, }, - this.key, + key, buffer, ); // Include public parameters to a cipher-buffer - return joinBuffers([iv, encryptedBuffer]); + return joinBuffers([AESHeader.encode({ iv }), encryptedBuffer]); } public async decrypt(buffer: ArrayBuffer) { - // Extract data of cipher-buffer - const iv = buffer.slice(0, this.ivSize); - const encryptedBuffer = buffer.slice(this.ivSize); - - return self.crypto.subtle - .decrypt( - { - name: 'AES-GCM', - //The initialization vector been used to encrypt - iv, + const { iv } = AESHeader.decode(new Uint8Array(buffer, 0, AESHeader.size)); - //The tagLength you used to encrypt (if any) - tagLength: 128, - }, - this.key, - encryptedBuffer, - ) - .catch((err) => { - if ( - err instanceof Error && - err.name === 'OperationError' && - err.message === '' - ) { - throw new IntegrityError('Decryption error. Integrity checks fails'); - } + const hkdf = new HKDFDerivedKeys(this.key, new Uint8Array(iv)); + const [key, messageIV] = await Promise.all([ + hkdf + .deriveBytes(32, 'msg') + .then((key) => + crypto.subtle.importKey('raw', key, 'AES-CTR', false, [ + 'encrypt', + 'decrypt', + ]), + ), + hkdf.deriveBytes(16, 'iv'), + ]); - throw err; - }); + return self.crypto.subtle.decrypt( + { + name: 'AES-CTR', + counter: messageIV, + length: 64, + }, + key, + new Uint8Array(buffer, AESHeader.size), + ); } } diff --git a/src/core/encryption/ciphers/Serpent/Serpent.test.ts b/src/core/encryption/ciphers/Serpent/Serpent.test.ts new file mode 100644 index 000000000..d5fbcc9a2 --- /dev/null +++ b/src/core/encryption/ciphers/Serpent/Serpent.test.ts @@ -0,0 +1,27 @@ +import { SerpentCipher } from '.'; + +const { encryptChunk } = vi.hoisted(() => ({ + encryptChunk: vi.fn((chunk: Uint8Array) => chunk), +})); + +vi.mock('leviathan-crypto', () => ({ + init: async () => undefined, + SerpentCtr: class { + beginEncrypt() {} + beginDecrypt() {} + encryptChunk = encryptChunk; + dispose() {} + }, +})); + +test('pads the last Serpent chunk to 16-byte alignment before encryptChunk', async () => { + const key = new Uint8Array(32); + const iv = new Uint8Array(16); + const cipher = new SerpentCipher(key, () => iv); + + await cipher.encrypt(new Uint8Array(17).buffer); + + expect(encryptChunk).toHaveBeenCalledWith( + expect.objectContaining({ byteLength: 32 }), + ); +}); diff --git a/src/core/encryption/ciphers/Serpent/index.ts b/src/core/encryption/ciphers/Serpent/index.ts new file mode 100644 index 000000000..6cd148460 --- /dev/null +++ b/src/core/encryption/ciphers/Serpent/index.ts @@ -0,0 +1,111 @@ +import { init, SerpentCtr } from 'leviathan-crypto'; +import { IEncryptionProcessor, RandomBytesGenerator } from '@core/encryption'; +import { getBlockPadding } from '@core/encryption/utils/buffers'; +import { bytes, struct, u8 } from '@core/encryption/utils/bytes/binstruct'; +import { BufferCursor } from '@core/encryption/utils/bytes/BufferCursor'; + +let initStatus: boolean | Promise; +export async function ensureWasmIsLoaded() { + if (!initStatus) { + initStatus = init(['serpent', 'sha2']).then(() => { + initStatus = true; + }); + } + + // Done + if (initStatus === true) return; + + // Wait + await initStatus; +} + +export const SERPENT_IV_SIZE = 16; +export const SERPENT_HEADER = struct({ + padding: u8(), + iv: bytes(SERPENT_IV_SIZE), +}); + +const chunkSize = 65536; + +export class SerpentCipher implements IEncryptionProcessor { + constructor( + private readonly key: Uint8Array, + private readonly getRandomBytes: RandomBytesGenerator, + ) {} + + public async encrypt(buffer: ArrayBuffer) { + await ensureWasmIsLoaded(); + const inputReader = new BufferCursor(buffer); + + const padding = getBlockPadding(buffer.byteLength, 16); + const iv = this.getRandomBytes(SERPENT_IV_SIZE); + + const output = new ArrayBuffer(SERPENT_HEADER.size + buffer.byteLength + padding); + const outputWriter = new BufferCursor(output); + + outputWriter.writeBytes(SERPENT_HEADER.encode({ padding, iv })); + + const cipher = new SerpentCtr({ dangerUnauthenticated: true }); + cipher.beginEncrypt(this.key, iv); + + let chunk; + while ((chunk = inputReader.readBytes(chunkSize))) { + // Align the chunk + const requiredPadding = getBlockPadding(chunk.byteLength, 16); + if (requiredPadding !== 0) { + if (requiredPadding !== padding) + throw new RangeError( + `Unexpected chunk size ${chunk.byteLength}. Expected padding is ${padding}, required padding is ${requiredPadding}`, + ); + + const chunkContent = chunk; + chunk = new Uint8Array(chunkContent.byteLength + requiredPadding); + chunk.set(new Uint8Array(chunkContent), 0); + } + + outputWriter.writeBytes(cipher.encryptChunk(chunk)); + } + + cipher.dispose(); + + return output; + } + + public async decrypt(buffer: ArrayBuffer) { + await ensureWasmIsLoaded(); + + const inputReader = new BufferCursor(buffer); + const header = inputReader.readBytes(SERPENT_HEADER.size); + if (!header || header.byteLength !== SERPENT_HEADER.size) + throw new RangeError('Cannot read header'); + + const { padding, iv } = SERPENT_HEADER.decode(header); + + if (inputReader.getRemainingBytes() % 16 !== 0) + throw new RangeError( + `Buffer payload is not aligned to 16 bytes. The actual size is ${inputReader.getRemainingBytes()}`, + ); + + const output = new ArrayBuffer(buffer.byteLength - padding - SERPENT_HEADER.size); + const outputWriter = new BufferCursor(output); + + const cipher = new SerpentCtr({ dangerUnauthenticated: true }); + cipher.beginDecrypt(this.key, iv); + + let chunk; + while ((chunk = inputReader.readBytes(chunkSize))) { + const pt = cipher.encryptChunk(chunk); + + const isLastChunk = inputReader.getRemainingBytes() === 0; + if (isLastChunk) { + outputWriter.writeBytes(pt.subarray(0, pt.length - padding)); + } else { + outputWriter.writeBytes(pt); + } + } + + cipher.dispose(); + + return output; + } +} diff --git a/src/core/encryption/ciphers/Twofish/index.ts b/src/core/encryption/ciphers/Twofish/index.ts index 69a44a627..26ace164f 100644 --- a/src/core/encryption/ciphers/Twofish/index.ts +++ b/src/core/encryption/ciphers/Twofish/index.ts @@ -1,118 +1,163 @@ -import { encrypt, makeSession } from 'twofish-ts'; +import { TwofishModule } from 'twofish'; +import twofishModule from 'twofish/twofish.wasm'; +import { bytes, struct, u8, u32 } from '@core/encryption/utils/bytes/binstruct'; +import { BufferCursor } from '@core/encryption/utils/bytes/BufferCursor'; +import { xor16 } from '@core/encryption/utils/xor'; import { CTRCipherMode } from '../../cipherModes/CTRCipherMode'; -import { BufferView, fillBuffer, joinBuffers } from '../../utils/buffers'; +import { alignBuffer, getBlockPadding } from '../../utils/buffers'; +import { HKDFDerivedKeys } from '../../utils/HKDFDerivedKeys'; -import { HeaderView, IEncryptionProcessor, RandomBytesGenerator } from '../..'; +import { IEncryptionProcessor } from '../..'; -const blockSize = 16; +export const TWOFISH_IV_SIZE = 12; +export const MASTER_IV_SIZE = 24; +export const TWOFISH_HEADER = struct({ + padding: u8(), + chunkSize: u32(), + iv: bytes(MASTER_IV_SIZE), +}); -export type TwofishBufferHeaderStruct = { - padding: number; - iv: ArrayBuffer; -}; - -export class TwofishBufferHeader implements HeaderView { - public readonly bufferSize = 128; - private readonly offsets = { - padding: 0, - iv: 1, - }; - - private readonly ivSize; - constructor(ivSize: number) { - this.ivSize = ivSize; +/** + * Twofish cipher implementation + */ +export class TwofishCTRCipher implements IEncryptionProcessor { + private readonly chunkSize; + constructor( + private readonly key: Uint8Array, + private readonly randomBytesGenerator: ( + byteLength: number, + ) => Uint8Array, + { chunkSize }: { chunkSize?: number } = {}, + ) { + const maxChunkSize = CTRCipherMode.getEncryptionLimits(16).maxBytes; + if (chunkSize === undefined) { + this.chunkSize = maxChunkSize; + } else if (chunkSize > maxChunkSize) { + throw new RangeError(`Chunk size is out of limit ${maxChunkSize}`); + } else { + this.chunkSize = chunkSize; + } } - public createBuffer(data: TwofishBufferHeaderStruct): ArrayBuffer { - const buffer = new ArrayBuffer(this.bufferSize); - const view = new DataView(buffer, 0); - const rawView = new BufferView(buffer); + private tfModule: Promise | null = null; + private getTwofishModule() { + if (!this.tfModule) { + this.tfModule = TwofishModule.load(twofishModule); + } - view.setUint8(this.offsets.padding, data.padding); - rawView.setBytes(data.iv, this.offsets.iv); - - return buffer; + return this.tfModule; } - public readBuffer(buffer: ArrayBuffer): TwofishBufferHeaderStruct { - if (buffer.byteLength < this.bufferSize) - throw new TypeError('Header buffer have too small size'); - - const view = new DataView(buffer, 0, this.bufferSize); - const rawView = new BufferView(buffer); - - return { - padding: view.getUint8(this.offsets.padding), - iv: rawView.getBytes(this.offsets.iv, this.offsets.iv + this.ivSize), - }; + public async load() { + await this.getTwofishModule(); } -} -/** - * Inner util to transform block with cipher - * Util creates a new buffer instead of mutate original buffer. - */ -function transformBuffer( - buffer: Uint8Array, - transformBlock: (offset: number, input: Uint8Array, output: Uint8Array) => void, -) { - const out = new Uint8Array(buffer.length); - for (let offset = 0; offset < buffer.length; offset += blockSize) { - transformBlock(offset, buffer, out); - } + public async encrypt(buffer: ArrayBuffer) { + const tf = await this.getTwofishModule(); - return out; -} + // We preallocate the RAM and re-use it, + // since only one XOR buffer is used at once, + // and we synchronously read the result before run next XOR + const xorBuffer = new Uint8Array(16); -// TODO: implement GCM mode https://en.wikipedia.org/wiki/Galois/Counter_Mode -/** - * Twofish cipher implementation - */ -export class TwofishCTRCipher implements IEncryptionProcessor { - private readonly ivSize = 96; - - private readonly key; - private readonly randomBytesGenerator: RandomBytesGenerator; - private readonly header; - private readonly ctrCipher; - constructor(cipher: Uint8Array, randomBytesGenerator: RandomBytesGenerator) { - this.randomBytesGenerator = randomBytesGenerator; - - this.key = makeSession(cipher); - this.header = new TwofishBufferHeader(this.ivSize); - this.ctrCipher = new CTRCipherMode(this.encryptBuffer); - } + const iv = this.randomBytesGenerator(MASTER_IV_SIZE); + const hkdf = new HKDFDerivedKeys(this.key, iv); - private readonly encryptBuffer = async (buffer: ArrayBuffer) => { - return transformBuffer(new Uint8Array(buffer), (offset, input, output) => { - encrypt(input, offset, output, offset, this.key); - }).buffer; - }; + const padding = getBlockPadding(buffer.byteLength, 16); + const alignedInputLength = buffer.byteLength + padding; - public async encrypt(buffer: ArrayBuffer) { - const [bufferView, padding] = fillBuffer(new Uint8Array(buffer)); + const output = new ArrayBuffer(TWOFISH_HEADER.size + alignedInputLength); + const outputWriter = new BufferCursor(output); - const iv = this.randomBytesGenerator(this.ivSize); - const encryptedBuffer = await this.ctrCipher.encrypt( - bufferView.buffer as ArrayBuffer, - iv, + outputWriter.writeBytes( + TWOFISH_HEADER.encode({ padding, iv, chunkSize: this.chunkSize }), ); - const header = this.header.createBuffer({ padding, iv }); - return joinBuffers([header, encryptedBuffer]); + const derivedKey = await hkdf.deriveBytes(16, `key`); + const chunksCount = Math.ceil(alignedInputLength / this.chunkSize); + const inputReader = new BufferCursor(buffer); + for (let i = 1; i <= chunksCount; i++) { + const derivedNonce = await hkdf.deriveBytes(TWOFISH_IV_SIZE, `nonce${i}`); + + const session = tf.createSession(derivedKey); + try { + const cipher = new CTRCipherMode( + (buffer: Uint8Array) => tf.encrypt(session, buffer), + (a, b) => xor16(xorBuffer, a, b), + ); + + const chunk = inputReader.readBytes(this.chunkSize); + if (!chunk) throw new RangeError(`Chunk #${i} is not found`); + + const encryptedChunk = await cipher.encrypt( + alignBuffer(chunk, 16), + derivedNonce, + ); + outputWriter.writeBytes(encryptedChunk); + } finally { + tf.destroySession(session); + } + } + + return output; } public async decrypt(buffer: ArrayBuffer) { - const { padding, iv } = this.header.readBuffer(buffer); - - const encryptedBuffer = new Uint8Array(buffer.slice(this.header.bufferSize)); - const decryptedBufferWithPaddings = await this.ctrCipher.decrypt( - encryptedBuffer.buffer, - iv, - ); - - const endOfDataOffset = decryptedBufferWithPaddings.byteLength - padding; - return decryptedBufferWithPaddings.slice(0, endOfDataOffset); + const inputReader = new BufferCursor(buffer); + + const header = inputReader.readBytes(TWOFISH_HEADER.size); + if (!header || header.byteLength !== TWOFISH_HEADER.size) + throw new RangeError('Cannot read header'); + + const { padding, iv, chunkSize } = TWOFISH_HEADER.decode(header); + if (chunkSize === 0) { + throw new RangeError('Invalid chunk size in header'); + } + + const tf = await this.getTwofishModule(); + + // We preallocate the RAM and re-use it, + // since only one XOR buffer is used at once, + // and we synchronously read the result before run next XOR + const xorBuffer = new Uint8Array(16); + + const hkdf = new HKDFDerivedKeys(this.key, new Uint8Array(iv)); + + const payloadSize = inputReader.getRemainingBytes(); + const output = new ArrayBuffer(payloadSize - padding); + const outputWriter = new BufferCursor(output); + + const derivedKey = await hkdf.deriveBytes(16, `key`); + const chunksCount = Math.ceil(payloadSize / chunkSize); + for (let i = 1; i <= chunksCount; i++) { + const derivedNonce = await hkdf.deriveBytes(TWOFISH_IV_SIZE, `nonce${i}`); + + const session = tf.createSession(derivedKey); + try { + const cipher = new CTRCipherMode( + (buffer: Uint8Array) => tf.encrypt(session, buffer), + (a, b) => xor16(xorBuffer, a, b), + ); + + const chunkBytes = inputReader.readBytes(chunkSize); + if (!chunkBytes) throw new RangeError(`Chunk #${i} is not found`); + + const decryptedChunk = await cipher.decrypt(chunkBytes, derivedNonce); + + const isLastChunk = i === chunksCount; + if (isLastChunk) { + outputWriter.writeBytes( + decryptedChunk.slice(0, decryptedChunk.length - padding), + ); + } else { + outputWriter.writeBytes(decryptedChunk); + } + } finally { + tf.destroySession(session); + } + } + + return output; } } diff --git a/src/core/encryption/ciphers/Twofish/twofish.spec.test.ts b/src/core/encryption/ciphers/Twofish/twofish.spec.test.ts new file mode 100644 index 000000000..e88ee6bee --- /dev/null +++ b/src/core/encryption/ciphers/Twofish/twofish.spec.test.ts @@ -0,0 +1,14 @@ +import { getRandomBytes } from '@core/encryption/utils/random'; + +import { TwofishCTRCipher } from '.'; + +test('Chunking', async () => { + const cipher = new TwofishCTRCipher(getRandomBytes(32), getRandomBytes, { + chunkSize: 16, + }); + + const pt = new Uint8Array(320).buffer; + const ct = await cipher.encrypt(pt); + + await expect(cipher.decrypt(ct)).resolves.toStrictEqual(pt); +}); diff --git a/src/core/encryption/ciphers/XChaCha20/index.ts b/src/core/encryption/ciphers/XChaCha20/index.ts new file mode 100644 index 000000000..1d8d460a4 --- /dev/null +++ b/src/core/encryption/ciphers/XChaCha20/index.ts @@ -0,0 +1,185 @@ +import sodium from 'libsodium-wrappers-sumo'; +import { IEncryptionProcessor, RandomBytesGenerator } from '@core/encryption'; +import { bytes, struct, u32, u64 } from '@core/encryption/utils/bytes/binstruct'; +import { BufferCursor } from '@core/encryption/utils/bytes/BufferCursor'; + +const emptyCounter = new Uint8Array(4); + +export class XChaCha20Poly1305 { + private readonly state; + constructor(masterKey: Uint8Array, iv: Uint8Array, hChaCha20Const?: Uint8Array) { + // Derive a key + this.state = sodium.ready.then(async () => { + const key = await sodium.crypto_core_hchacha20( + new Uint8Array(iv).slice(0, 16), + masterKey, + hChaCha20Const ?? null, + ); + + const nonce = new Uint8Array(12); + nonce.set(new Uint8Array(iv).slice(16), 4); + + return { key, nonce }; + }); + } + + public async dispose() { + const state = await this.state; + + sodium.memzero(state.nonce); + sodium.memzero(state.key); + } + + public async encrypt( + plaintext: Uint8Array, + { aad, counter }: { aad?: Uint8Array; counter?: Uint8Array } = {}, + ) { + const { key, nonce } = await this.state; + nonce.set(counter ? counter : emptyCounter); + + return sodium.crypto_aead_chacha20poly1305_ietf_encrypt_detached( + plaintext, + aad ?? null, + null, + nonce, + key, + ); + } + + public async decrypt( + ciphertext: Uint8Array, + mac: Uint8Array, + { aad, counter }: { aad?: Uint8Array; counter?: Uint8Array } = {}, + ) { + const { key, nonce } = await this.state; + nonce.set(counter ? counter : emptyCounter); + + return sodium.crypto_aead_chacha20poly1305_ietf_decrypt_detached( + null, + ciphertext, + mac, + aad ?? null, + nonce, + key, + ); + } +} + +const ChaChaHeader = struct({ + nonce: bytes(24), + chunkSize: u32(), + length: u64(), +}); + +const createCounter = () => { + const buffer = new Uint8Array(4); + const view = new DataView(buffer.buffer); + const increment = () => { + // use little endian bytes order to comply RFC 8439 Section 2.3 + view.setUint32(0, view.getUint32(0, true) + 1, true); + }; + + return { buffer, view, increment }; +}; + +const TAG_SIZE = 16; + +export class XChaCha20Cipher implements IEncryptionProcessor { + private readonly chunkSize; + constructor( + private readonly key: Uint8Array, + private readonly randomBytes: RandomBytesGenerator, + readonly config: { chunkSize?: number } = {}, + ) { + this.chunkSize = config.chunkSize ?? 4096; + } + + public async encrypt(buffer: ArrayBuffer) { + const nonce = this.randomBytes(24); + const cipher = new XChaCha20Poly1305(this.key, nonce); + + try { + // Allocate output buffer as buffer size + chunks overhead + header size + const chunksCount = Math.ceil(buffer.byteLength / this.chunkSize); + const outBuffer = new Uint8Array( + ChaChaHeader.size + buffer.byteLength + chunksCount * TAG_SIZE, + ); + + const input = new BufferCursor(buffer); + const output = new BufferCursor(outBuffer.buffer); + + output.writeBytes( + ChaChaHeader.encode({ + nonce, + length: BigInt(buffer.byteLength), + chunkSize: this.chunkSize, + }), + ); + + // Start encryption + const counter = createCounter(); + while (true) { + const chunkBytes = input.readBytes(this.chunkSize); + + // We break the loop below, when no more bytes remains in reader + // So if we can't read bytes here, it is unexpected and it is critical situation + if (chunkBytes === null) + throw new Error('Unexpected end of input buffer'); + + const isLastChunk = input.getRemainingBytes() === 0; + + // TODO: add final tag (aad) + const { ciphertext, mac } = await cipher.encrypt(chunkBytes, { + counter: counter.buffer, + }); + output.writeBytes(ciphertext); + output.writeBytes(mac); + + if (isLastChunk) break; + counter.increment(); + } + + return outBuffer.buffer; + } catch (error) { + cipher.dispose(); + throw error; + } + } + + public async decrypt(buffer: ArrayBuffer) { + const input = new BufferCursor(buffer); + + const metaHeader = input.readBytes(ChaChaHeader.size); + if (!metaHeader) throw RangeError('Cannot read meta header'); + const meta = ChaChaHeader.decode(metaHeader); + + // Allocate an output buffer + const outBuffer = new Uint8Array(Number(meta.length)).buffer; + const output = new BufferCursor(outBuffer); + + // Decrypt data + const cipher = new XChaCha20Poly1305(this.key, meta.nonce); + const counter = createCounter(); + while (true) { + const bytes = input.readBytes(meta.chunkSize + TAG_SIZE); + + // End of data + if (bytes === null) break; + + const dataBytesLen = bytes.byteLength - TAG_SIZE; + if (0 > dataBytesLen) throw new RangeError('Too short chunk'); + + const data = bytes.subarray(0, dataBytesLen); + const mac = bytes.subarray(dataBytesLen); + + const plaintext = await cipher.decrypt(data, mac, { + counter: counter.buffer, + }); + counter.increment(); + + output.writeBytes(plaintext); + } + + return outBuffer; + } +} diff --git a/src/core/encryption/ciphers/XChaCha20/xchacha.spec.test.ts b/src/core/encryption/ciphers/XChaCha20/xchacha.spec.test.ts new file mode 100644 index 000000000..8429d1c7a --- /dev/null +++ b/src/core/encryption/ciphers/XChaCha20/xchacha.spec.test.ts @@ -0,0 +1,177 @@ +/* eslint-disable no-bitwise */ +/* eslint-disable @cspell/spellchecker */ +import sodium from 'libsodium-wrappers-sumo'; + +import { XChaCha20Poly1305 } from '.'; + +describe('draft-arciszewski-xchacha-01', () => { + // Source: https://www.potaroo.net/ietf/all-ids/draft-arciszewski-xchacha-01.html + + test('HChaCha20 test vectors', async () => { + // Source: https://www.potaroo.net/ietf/all-ids/draft-arciszewski-xchacha-01.html#rfc.section.2.2.1 + await sodium.ready; + + const key = Uint8Array.from([ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, + 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + ]); + + const nonce = Uint8Array.from([ + 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00, 0x31, + 0x41, 0x59, 0x27, + ]); + + expect(sodium.crypto_core_hchacha20(nonce, key, null)).toStrictEqual( + Uint8Array.from([ + 0x82, 0x41, 0x3b, 0x42, 0x27, 0xb2, 0x7b, 0xfe, 0xd3, 0x0e, 0x42, 0x50, + 0x8a, 0x87, 0x7d, 0x73, 0xa0, 0xf9, 0xe4, 0xd5, 0x8a, 0x74, 0xa8, 0x53, + 0xc1, 0x2e, 0xc4, 0x13, 0x26, 0xd3, 0xec, 0xdc, + ]), + ); + }); + + test('XChaCha20', async () => { + await sodium.ready; + + const plaintext = Buffer.from( + `5468652064686f6c65202870726f6e6f756e6365642022646f6c65222920697320616c736f206b6e6f776e2061732074686520417369617469632077696c6420646f672c2072656420646f672c20616e642077686973746c696e6720646f672e2049742069732061626f7574207468652073697a65206f662061204765726d616e20736865706865726420627574206c6f6f6b73206d6f7265206c696b652061206c6f6e672d6c656767656420666f782e205468697320686967686c7920656c757369766520616e6420736b696c6c6564206a756d70657220697320636c6173736966696564207769746820776f6c7665732c20636f796f7465732c206a61636b616c732c20616e6420666f78657320696e20746865207461786f6e6f6d69632066616d696c792043616e696461652e`, + 'hex', + ); + + expect(plaintext.toString('ascii')).toBe( + `The dhole (pronounced "dole") is also known as the Asiatic wild dog, red dog, and whistling dog. It is about the size of a German shepherd but looks more like a long-legged fox. This highly elusive and skilled jumper is classified with wolves, coyotes, jackals, and foxes in the taxonomic family Canidae.`, + ); + + const key = Buffer.from( + `808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f`, + 'hex', + ); + + const iv = Buffer.from(`404142434445464748494a4b4c4d4e4f5051525354555658`, 'hex'); + expect(iv.toString('ascii')).toBe(`@ABCDEFGHIJKLMNOPQRSTUVX`); + + const expectedCt = Buffer.from( + `4559abba4e48c16102e8bb2c05e6947f50a786de162f9b0b7e592a9b53d0d4e98d8d6410d540a1a6375b26d80dace4fab52384c731acbf16a5923c0c48d3575d4d0d2c673b666faa731061277701093a6bf7a158a8864292a41c48e3a9b4c0daece0f8d98d0d7e05b37a307bbb66333164ec9e1b24ea0d6c3ffddcec4f68e7443056193a03c810e11344ca06d8ed8a2bfb1e8d48cfa6bc0eb4e2464b748142407c9f431aee769960e15ba8b96890466ef2457599852385c661f752ce20f9da0c09ab6b19df74e76a95967446f8d0fd415e7bee2a12a114c20eb5292ae7a349ae577820d5520a1f3fb62a17ce6a7e68fa7c79111d8860920bc048ef43fe84486ccb87c25f0ae045f0cce1e7989a9aa220a28bdd4827e751a24a6d5c62d790a66393b93111c1a55dd7421a10184974c7c5"`, + 'hex', + ); + + expect(sodium.crypto_stream_xchacha20_xor(plaintext, iv, key)).toStrictEqual( + new Uint8Array(expectedCt), + ); + }); + + describe('XChaCha20-Poly1305 AEAD', () => { + const plaintext = Buffer.from( + `4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e20776f756c642062652069742e`, + 'hex', + ); + + const aad = Buffer.from('50515253c0c1c2c3c4c5c6c7', 'hex'); + + const key = Buffer.from( + `808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f`, + 'hex', + ); + + const iv = Buffer.from(`404142434445464748494a4b4c4d4e4f5051525354555657`, 'hex'); + + const poly1305Key = Buffer.from( + `7b191f80f361f099094f6f4b8fb97df847cc6873a8f2b190dd73807183f907d5`, + 'hex', + ); + + const expectedCt = Buffer.from( + `bd6d179d3e83d43b9576579493c0e939572a1700252bfaccbed2902c21396cbb731c7f1b0b4aa6440bf3a82f4eda7e39ae64c6708c54c216cb96b72e1213b4522f8c9ba40db5d945b11b69b982c1bb9e3f3fac2bc369488f76b2383565d3fff921f9664c97637da9768812f615c68b13b52e`, + 'hex', + ); + + const tag = Buffer.from(`c0875924c1c7987947deafd8780acf49`, 'hex'); + + test('Test data is correct', () => { + expect(new Uint8Array(aad)).toStrictEqual( + Uint8Array.from([ + 0x50, 0x51, 0x52, 0x53, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, + 0xc7, + ]), + ); + + expect(plaintext.toString('ascii')).toBe( + `Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.`, + ); + + expect(iv.toString('ascii')).toBe(`@ABCDEFGHIJKLMNOPQRSTUVW`); + }); + + test('Poly 1305 key derivation', async () => { + await sodium.ready; + + expect( + sodium.crypto_stream_chacha20_xor_ic( + new Uint8Array(32), + new Uint8Array(iv).slice(16), + 0, + sodium.crypto_core_hchacha20( + new Uint8Array(iv).slice(0, 16), + key, + null, + ), + ), + ).toStrictEqual(new Uint8Array(poly1305Key)); + }); + + test('XChaCha20-Poly1305 wrapper', async () => { + const cipher = new XChaCha20Poly1305(key, iv); + + const encryptionResult = await cipher.encrypt(plaintext, { aad }); + expect(encryptionResult).toStrictEqual({ + ciphertext: new Uint8Array(expectedCt), + mac: new Uint8Array(tag), + }); + + await expect( + cipher.decrypt(encryptionResult.ciphertext, encryptionResult.mac, { + aad, + }), + ).resolves.toStrictEqual(new Uint8Array(plaintext)); + + // Cannot decrypt with incorrect mac + const tamperedMac = new Uint8Array(encryptionResult.mac); + tamperedMac[0] ^= 0x01; + await expect( + cipher.decrypt(encryptionResult.ciphertext, tamperedMac, { aad }), + ).rejects.toThrow(); + }); + + test('XChaCha20-Poly1305 detached mode', async () => { + await sodium.ready; + + expect( + sodium.crypto_aead_xchacha20poly1305_ietf_encrypt_detached( + plaintext, + aad, + null, + iv, + key, + ), + ).toStrictEqual({ + ciphertext: new Uint8Array(expectedCt), + mac: new Uint8Array(tag), + }); + }); + + test('XChaCha20-Poly1305 ciphertext', async () => { + await sodium.ready; + + expect( + sodium.crypto_aead_xchacha20poly1305_ietf_encrypt( + plaintext, + aad, + null, + iv, + key, + ), + ).toStrictEqual(new Uint8Array(Buffer.concat([expectedCt, tag]))); + }); + }); +}); diff --git a/src/core/encryption/index.ts b/src/core/encryption/index.ts index 74c44844f..9aa8d757e 100644 --- a/src/core/encryption/index.ts +++ b/src/core/encryption/index.ts @@ -20,4 +20,4 @@ export interface HeaderView { /** * Creates buffer with random values */ -export type RandomBytesGenerator = (bufferSize: number) => ArrayBuffer; +export type RandomBytesGenerator = (bufferSize: number) => Uint8Array; diff --git a/src/core/encryption/processors/BufferIntegrityProcessor.test.ts b/src/core/encryption/processors/BufferIntegrityProcessor.test.ts new file mode 100644 index 000000000..119f539a6 --- /dev/null +++ b/src/core/encryption/processors/BufferIntegrityProcessor.test.ts @@ -0,0 +1,90 @@ +/* eslint-disable no-bitwise */ +import { webcrypto } from 'node:crypto'; + +import { createFakeRandomBytesGenerator } from '../__tests__/random'; +import { getRandomBytes } from '../utils/random'; +import { BufferIntegrityProcessor } from './BufferIntegrityProcessor'; + +vi.stubGlobal('crypto', webcrypto); +vi.stubGlobal('self', globalThis); + +test('Valid data must be successfully verified', async () => { + const key = getRandomBytes(32); + const integrity = new BufferIntegrityProcessor(key); + + const data = getRandomBytes(1024); + const dataHex = Buffer.from(data).toString('hex'); + const blob = await integrity.encrypt(data.buffer); + await expect( + integrity.decrypt(blob).then((buffer) => Buffer.from(buffer).toString('hex')), + ).resolves.toBe(dataHex); +}); + +test('Modified data must throw error', async () => { + const key = getRandomBytes(32); + const integrity = new BufferIntegrityProcessor(key); + + const data = getRandomBytes(1024); + const blob = await integrity.encrypt(data.buffer); + + const blobView = new Uint8Array(blob); + blobView[blobView.length - 1] = blobView.at(-1)! ^ 1; + + await expect(integrity.decrypt(blob)).rejects.toMatchObject({ + name: 'IntegrityError', + message: expect.stringContaining('Integrity violation'), + }); +}); + +test('Too small buffer must be rejected immediately', async () => { + const integrity = new BufferIntegrityProcessor(getRandomBytes(32)); + await expect(integrity.decrypt(getRandomBytes(2).buffer)).rejects.toMatchObject({ + name: 'IntegrityError', + message: expect.stringContaining('short buffer'), + }); +}); + +test('Valid data must throw error with invalid key', async () => { + const key1 = getRandomBytes(32); + const key2 = getRandomBytes(32); + + const data = getRandomBytes(1024); + const blob = await new BufferIntegrityProcessor(key1).encrypt(data.buffer); + + await expect(new BufferIntegrityProcessor(key2).decrypt(blob)).rejects.toMatchObject({ + name: 'IntegrityError', + message: expect.stringContaining('Integrity violation'), + }); +}); + +test('Error must be thrown when key of invalid size is provided', async () => { + await expect( + new BufferIntegrityProcessor(getRandomBytes(0)).encrypt( + getRandomBytes(1024).buffer, + ), + ).rejects.toThrow('Zero-length key is not supported'); + + for (const length of [1, 31, 33, 64, 128, 1000]) { + await expect( + new BufferIntegrityProcessor(getRandomBytes(length)).encrypt( + getRandomBytes(32).buffer, + ), + ).rejects.toThrow('Invalid key length'); + } +}); + +test('The HMAC output must match a snapshot', async () => { + const getPseudoRandomBytes = createFakeRandomBytesGenerator(0); + + const key = getPseudoRandomBytes(32); + const integrity = new BufferIntegrityProcessor(key); + + const data = getPseudoRandomBytes(128); + const dataHex = Buffer.from(data).toString('hex'); + + await expect( + integrity + .encrypt(data.buffer) + .then((buffer) => Buffer.from(buffer).toString('hex')), + ).resolves.toMatchSnapshot(dataHex); +}); diff --git a/src/core/encryption/processors/BufferIntegrityProcessor.ts b/src/core/encryption/processors/BufferIntegrityProcessor.ts index 54b60c0b8..67064f157 100644 --- a/src/core/encryption/processors/BufferIntegrityProcessor.ts +++ b/src/core/encryption/processors/BufferIntegrityProcessor.ts @@ -1,35 +1,5 @@ -import crc32 from 'crc/calculators/crc32'; - import { joinBuffers } from '../utils/buffers'; -import { HeaderView, IEncryptionProcessor } from '..'; - -export type IntegrityHeaderStruct = { - crc32: number; -}; - -export class IntegrityHeader implements HeaderView { - public readonly bufferSize = 8; - - public createBuffer(data: IntegrityHeaderStruct): ArrayBuffer { - const buffer = new ArrayBuffer(this.bufferSize); - const view = new DataView(buffer, 0); - - view.setInt32(0, data.crc32); - - return buffer; - } - - public readBuffer(buffer: ArrayBuffer): IntegrityHeaderStruct { - if (buffer.byteLength < this.bufferSize) - throw new TypeError('Header buffer have too small size'); - - const view = new DataView(buffer, 0, this.bufferSize); - - return { - crc32: view.getInt32(0), - }; - } -} +import { IEncryptionProcessor } from '..'; export class IntegrityError extends TypeError { public readonly name = 'IntegrityError'; @@ -40,31 +10,46 @@ export class IntegrityError extends TypeError { * and check the sum with actual buffer checksum during decryption. */ export class BufferIntegrityProcessor implements IEncryptionProcessor { - private readonly integrityHeader; - constructor() { - this.integrityHeader = new IntegrityHeader(); - } + constructor(private readonly key: Uint8Array) {} public encrypt = async (buffer: ArrayBuffer) => { - const bufferSum = crc32(new Uint8Array(buffer)); - const header = this.integrityHeader.createBuffer({ - crc32: bufferSum, - }); + const key = await crypto.subtle.importKey( + 'raw', + this.key, + { name: 'HMAC', hash: 'SHA-256', length: 256 }, + false, + ['sign', 'verify'], + ); - return joinBuffers([header, buffer]); + const signature = await crypto.subtle.sign('HMAC', key, buffer); + + return joinBuffers([signature, buffer]); }; /** * @throws `IntegrityError` when check sum do not match */ public decrypt = async (buffer: ArrayBuffer) => { - const header = this.integrityHeader.readBuffer(buffer); - const slicedBuffer = buffer.slice(this.integrityHeader.bufferSize); - - const bufferSum = crc32(new Uint8Array(slicedBuffer)); - if (bufferSum !== header.crc32) - throw new IntegrityError('Decryption error. Check sum does not match'); - - return slicedBuffer; + if (buffer.byteLength < 32) + throw new IntegrityError('Integrity violation. Too short buffer'); + + const key = await crypto.subtle.importKey( + 'raw', + this.key, + { name: 'HMAC', hash: 'SHA-256', length: 256 }, + false, + ['sign', 'verify'], + ); + const signature = new Uint8Array(buffer, 0, 32); + const data = new Uint8Array(buffer, 32); + + const isValidSignature = await crypto.subtle.verify('HMAC', key, signature, data); + + if (!isValidSignature) + throw new IntegrityError( + 'Integrity violation. HMAC signature check is failed', + ); + + return data.slice().buffer; }; } diff --git a/src/core/encryption/processors/BufferSizeObfuscationProcessor.ts b/src/core/encryption/processors/BufferSizeObfuscationProcessor.ts index 0d3f20cf2..a4881b077 100644 --- a/src/core/encryption/processors/BufferSizeObfuscationProcessor.ts +++ b/src/core/encryption/processors/BufferSizeObfuscationProcessor.ts @@ -62,7 +62,7 @@ export class BufferSizeObfuscationProcessor implements IEncryptionProcessor { // Generate random padding size // Here we create a buffer with random 4 byte which is 32 bits, // then creates a typed array from a buffer and takes a first number - const [randomNumber] = new Uint32Array(this.randomBytesGenerator(4)); + const [randomNumber] = new Uint32Array(this.randomBytesGenerator(4).buffer); // Clamp padding size to a limit const paddingSizeLimitForBuffer = this.getPaddingSizeLimit(buffer.byteLength); diff --git a/src/core/encryption/processors/__snapshots__/BufferIntegrityProcessor.test.ts.snap b/src/core/encryption/processors/__snapshots__/BufferIntegrityProcessor.test.ts.snap new file mode 100644 index 000000000..b06a4b71d --- /dev/null +++ b/src/core/encryption/processors/__snapshots__/BufferIntegrityProcessor.test.ts.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`The HMAC output must match a snapshot > 3421544059422d56d16f7abbc1f68489626635a50563f7777d13012b00c05639fde378de1340b9d2c81838725c08d2c78fdf0d94b9334ea27700329e9e3642f0962cc8a48d5775668c9a438dc88303a3a814d627d90eab1ebae9cabed127eda23ab283c6d1455113c4daf471ac3a21f1d4db8ba914ac0e2b3ac730145610d6ea 1`] = `"efdd87e39cb0bc07dd95e339c44bda0b900caca228d6ba7b6aa79143abbf60313421544059422d56d16f7abbc1f68489626635a50563f7777d13012b00c05639fde378de1340b9d2c81838725c08d2c78fdf0d94b9334ea27700329e9e3642f0962cc8a48d5775668c9a438dc88303a3a814d627d90eab1ebae9cabed127eda23ab283c6d1455113c4daf471ac3a21f1d4db8ba914ac0e2b3ac730145610d6ea"`; diff --git a/src/core/encryption/utils/HKDFDerivedKeys.ts b/src/core/encryption/utils/HKDFDerivedKeys.ts new file mode 100644 index 000000000..986ce54f0 --- /dev/null +++ b/src/core/encryption/utils/HKDFDerivedKeys.ts @@ -0,0 +1,37 @@ +export class HKDFDerivedKeys { + private readonly key; + constructor( + key: Uint8Array, + private readonly iv: Uint8Array, + ) { + this.key = crypto.subtle.importKey('raw', key, 'HKDF', false, [ + 'deriveKey', + 'deriveBits', + ]); + } + + public stringToBuffer(text: string) { + return new TextEncoder().encode(text); + } + + public async deriveBits(length: number, context: string | Uint8Array) { + const key = await this.key; + + return await crypto.subtle.deriveBits( + { + name: 'HKDF', + hash: 'SHA-256', + salt: this.iv, + info: + typeof context === 'string' ? this.stringToBuffer(context) : context, + }, + key, + length, + ); + } + + public async deriveBytes(length: number, context: string | Uint8Array) { + const buffer = await this.deriveBits(length * 8, context); + return new Uint8Array(buffer); + } +} diff --git a/src/core/encryption/utils/buffers.ts b/src/core/encryption/utils/buffers.ts index bf93cef96..a71a92cfa 100644 --- a/src/core/encryption/utils/buffers.ts +++ b/src/core/encryption/utils/buffers.ts @@ -17,18 +17,24 @@ export const joinBuffers = (buffers: (ArrayBufferLike | Buffer | TypedArray)[]) }; /** - * Fill buffer with paddings to ensure buffer size multiple 16 - * Before use original data you have to remove padding + * Ensure correct buffer size and adds padding if needed. + * + * In case the buffer have correct size, the same buffer will be returned. + * Otherwise will be returned new buffer. */ -export function fillBuffer(buffer: Uint8Array, blockSize = 16): [Uint8Array, number] { - const padding = Math.ceil(buffer.length / blockSize) * blockSize - buffer.length; - if (padding === 0) return [buffer, 0]; +export function alignBuffer(buffer: Uint8Array, boxSize: number) { + const remainingBytes = buffer.length % boxSize; + if (remainingBytes === 0) return buffer; + + const padding = boxSize - remainingBytes; + const alignedBuffer = new Uint8Array(buffer.length + padding); + alignedBuffer.set(new Uint8Array(buffer), 0); - // Create new buffer with padding - const out = new Uint8Array(buffer.length + padding); - out.set(buffer); + return alignedBuffer; +} - return [out, padding]; +export function getBlockPadding(totalSize: number, blockSize = 16) { + return Math.ceil(totalSize / blockSize) * blockSize - totalSize; } /** diff --git a/src/core/encryption/utils/bytes/BufferCursor.test.ts b/src/core/encryption/utils/bytes/BufferCursor.test.ts new file mode 100644 index 000000000..172bfda05 --- /dev/null +++ b/src/core/encryption/utils/bytes/BufferCursor.test.ts @@ -0,0 +1,85 @@ +import { BufferCursor } from './BufferCursor'; + +describe('Writing', () => { + test('Data must be appended', () => { + const buffer = new ArrayBuffer(10); + const cursor = new BufferCursor(buffer); + + expect(new Uint8Array(buffer)).toStrictEqual(new Uint8Array(10).fill(0)); + + cursor.writeBytes(new Uint8Array([1, 2, 3])); + expect(new Uint8Array(buffer)).toStrictEqual( + new Uint8Array([1, 2, 3, ...Array(7).fill(0)]), + ); + + cursor.writeBytes(new Uint8Array([4, 5, 6])); + cursor.writeBytes(new Uint8Array([7])); + cursor.writeBytes(new Uint8Array([8, 9, 10])); + + expect(new Uint8Array(buffer)).toStrictEqual( + new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + ); + }); + + test('Error must be thrown for attempt of insertion a data larger than buffer size', () => { + const buffer = new ArrayBuffer(10); + const cursor = new BufferCursor(buffer); + + expect(new Uint8Array(buffer)).toStrictEqual(new Uint8Array(10).fill(0)); + + // Start writing + cursor.writeBytes(new Uint8Array([1, 2, 3])); + expect(new Uint8Array(buffer)).toStrictEqual( + new Uint8Array([1, 2, 3, ...Array(7).fill(0)]), + ); + + // + expect(() => cursor.writeBytes(new Uint8Array(10))).toThrow( + 'Cannot insert 10 bytes. Remaining bytes in buffer: 7', + ); + + // Continue writing + cursor.writeBytes(new Uint8Array([4])); + expect(new Uint8Array(buffer)).toStrictEqual( + new Uint8Array([1, 2, 3, 4, ...Array(6).fill(0)]), + ); + }); + + test('Error must be thrown if no space is left', () => { + const buffer = new ArrayBuffer(10); + const cursor = new BufferCursor(buffer); + + // Write full buffer + cursor.writeBytes(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])); + expect(new Uint8Array(buffer)).toStrictEqual( + new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + ); + + expect(() => cursor.writeBytes(new Uint8Array([1]))).toThrow( + 'Cannot insert 1 bytes. Remaining bytes in buffer: 0', + ); + }); +}); + +describe('Reading', () => { + test('Reader must return bytes after cursor', () => { + const buffer = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).buffer; + const cursor = new BufferCursor(buffer); + + expect(cursor.readBytes(3)).toStrictEqual(new Uint8Array([1, 2, 3])); + expect(cursor.readBytes(3)).toStrictEqual(new Uint8Array([4, 5, 6])); + expect(cursor.readBytes(3)).toStrictEqual(new Uint8Array([7, 8, 9])); + expect(cursor.readBytes(3)).toStrictEqual(new Uint8Array([10])); + }); + + test('Reader must return null when no bytes left after cursor', () => { + const buffer = new Uint8Array([1, 2, 3]).buffer; + const cursor = new BufferCursor(buffer); + + expect(cursor.readBytes(3)).toStrictEqual(new Uint8Array([1, 2, 3])); + + expect(cursor.readBytes(3)).toBe(null); + expect(cursor.readBytes(3)).toBe(null); + expect(cursor.readBytes(3)).toBe(null); + }); +}); diff --git a/src/core/encryption/utils/bytes/BufferCursor.ts b/src/core/encryption/utils/bytes/BufferCursor.ts new file mode 100644 index 000000000..a8cdb6ac8 --- /dev/null +++ b/src/core/encryption/utils/bytes/BufferCursor.ts @@ -0,0 +1,31 @@ +export class BufferCursor { + private offset = 0; + constructor(private readonly buffer: ArrayBuffer) {} + + getRemainingBytes() { + return this.buffer.byteLength - this.offset; + } + + readBytes(length: number): Uint8Array | null { + const remaining = this.getRemainingBytes(); + + if (remaining <= 0) return null; + + length = Math.min(length, remaining); + + const slice = new Uint8Array(this.buffer, this.offset, length); + this.offset += length; + return slice; + } + + writeBytes(data: Uint8Array): void { + const remaining = this.getRemainingBytes(); + if (data.length > remaining) + throw new RangeError( + `Cannot insert ${data.length} bytes. Remaining bytes in buffer: ${remaining}`, + ); + + new Uint8Array(this.buffer, this.offset, data.length).set(data); + this.offset += data.length; + } +} diff --git a/src/core/encryption/utils/bytes/binstruct.test.ts b/src/core/encryption/utils/bytes/binstruct.test.ts new file mode 100644 index 000000000..27cd60ebb --- /dev/null +++ b/src/core/encryption/utils/bytes/binstruct.test.ts @@ -0,0 +1,111 @@ +import { bytes, struct, transform, u8, u16, u32, u64 } from './binstruct'; + +const enc = new TextEncoder(); +const dec = new TextDecoder(); + +describe('binstruct', () => { + describe('struct', () => { + it('calculates total size', () => { + const Header = struct({ version: u8(), flags: u8(), length: u32() }); + expect(Header.size).toBe(6); + }); + + it('encodes and decodes flat struct', () => { + const Header = struct({ version: u8(), flags: u8(), length: u32() }); + const buf = Header.encode({ version: 1, flags: 0, length: 1024 }); + expect(Header.decode(buf)).toStrictEqual({ + version: 1, + flags: 0, + length: 1024, + }); + }); + + it('decodes struct that has been built manually', () => { + const buf = new Uint8Array(6); + buf.set(new Uint8Array([10]), 0); + buf.set(new Uint8Array([123]), 1); + buf.set(new Uint8Array(new Uint32Array([1024]).buffer), 2); + + const Header = struct({ version: u8(), flags: u8(), length: u32() }); + expect(Header.decode(buf)).toStrictEqual({ + version: 10, + flags: 123, + length: 1024, + }); + }); + + it('encodes and decodes nested struct', () => { + const Header = struct({ version: u8(), flags: u8(), length: u32() }); + const Packet = struct({ header: Header.field(), payload: bytes(4) }); + const payload = new Uint8Array([0xde, 0xad, 0xbe, 0xef]); + + const buf = Packet.encode({ + header: { version: 1, flags: 0, length: 4 }, + payload, + }); + + expect(Packet.size).toBe(10); + expect(Packet.decode(buf)).toStrictEqual({ + header: { version: 1, flags: 0, length: 4 }, + payload, + }); + }); + + it('rejects malformed fixed-size byte fields', () => { + const Header = struct({ iv: bytes(16) }); + + expect(() => Header.encode({ iv: new Uint8Array(15) })).toThrow(/16/); + expect(() => Header.encode({ iv: new Uint8Array(17) })).toThrow(/16/); + }); + }); + + describe('transform', () => { + it('encodes and decodes bytes as a string', () => { + const Message = struct({ + id: u16(), + text: transform( + bytes(32), + (str: string) => { + const buf = new Uint8Array(32); + buf.set(enc.encode(str).subarray(0, 32)); + return buf; + }, + (buf) => dec.decode(buf).replace(/\0+$/, ''), + ), + }); + + const buf = Message.encode({ id: 42, text: 'hello world' }); + expect(Message.decode(buf)).toStrictEqual({ id: 42, text: 'hello world' }); + }); + + it('encodes and decodes bytes as a mac address', () => { + const MacAddress = struct({ + address: transform( + bytes(6), + (hex: string) => + new Uint8Array(hex.split(':').map((b) => parseInt(b, 16))), + (buf) => + Array.from(buf) + .map((b) => b.toString(16).padStart(2, '0')) + .join(':'), + ), + }); + + const buf = MacAddress.encode({ address: 'de:ad:be:ef:00:01' }); + expect(MacAddress.decode(buf)).toStrictEqual({ + address: 'de:ad:be:ef:00:01', + }); + }); + }); + + describe('u64', () => { + it('encodes and decodes big endian u64', () => { + const Timestamp = struct({ seconds: u64('BE'), nanos: u32('BE') }); + const buf = Timestamp.encode({ seconds: 1700000000n, nanos: 500000000 }); + expect(Timestamp.decode(buf)).toStrictEqual({ + seconds: 1700000000n, + nanos: 500000000, + }); + }); + }); +}); diff --git a/src/core/encryption/utils/bytes/binstruct.ts b/src/core/encryption/utils/bytes/binstruct.ts new file mode 100644 index 000000000..6c799f09d --- /dev/null +++ b/src/core/encryption/utils/bytes/binstruct.ts @@ -0,0 +1,122 @@ +type Endian = 'LE' | 'BE'; + +interface Field { + size: number; + encode(value: TInput, view: DataView, offset: number): void; + decode(view: DataView, offset: number): TOutput; +} + +type Shape = Record>; + +type EncodeShape = { + [K in keyof S]: S[K] extends Field ? I : never; +}; + +type DecodeShape = { + [K in keyof S]: S[K] extends Field ? O : never; +}; + +// ── Primitives ──────────────────────────────────────────────── + +export const u8 = (): Field => ({ + size: 1, + encode: (val, view, off) => view.setUint8(off, val), + decode: (view, off) => view.getUint8(off), +}); + +export const u16 = (e: Endian = 'LE'): Field => ({ + size: 2, + encode: (val, view, off) => view.setUint16(off, val, e === 'LE'), + decode: (view, off) => view.getUint16(off, e === 'LE'), +}); + +export const u32 = (e: Endian = 'LE'): Field => ({ + size: 4, + encode: (val, view, off) => view.setUint32(off, val, e === 'LE'), + decode: (view, off) => view.getUint32(off, e === 'LE'), +}); + +export const u64 = (e: Endian = 'LE'): Field => ({ + size: 8, + encode: (val, view, off) => view.setBigUint64(off, val, e === 'LE'), + decode: (view, off) => view.getBigUint64(off, e === 'LE'), +}); + +export const bytes = (length: number): Field> => ({ + size: length, + encode: (val, view, off) => { + if (val.length !== length) { + throw new RangeError(`Expected ${length} bytes, got ${val.length}`); + } + + return new Uint8Array(view.buffer, view.byteOffset + off, length).set(val); + }, + decode: (view, off) => + new Uint8Array(view.buffer).slice( + view.byteOffset + off, + view.byteOffset + off + length, + ), +}); + +// ── Transform ───────────────────────────────────────────────── + +export const transform = ( + field: Field, + encode: (value: TInput) => TBase, + decode: (value: TBase) => TOutput, +): Field => ({ + size: field.size, + encode: (val, view, off) => field.encode(encode(val), view, off), + decode: (view, off) => decode(field.decode(view, off)), +}); + +// ── Struct ──────────────────────────────────────────────────── + +interface Struct { + size: number; + encode(obj: EncodeShape): Uint8Array; + decode(buffer: Uint8Array): DecodeShape; + field(): Field, DecodeShape>; +} + +export const struct = (fields: S): Struct => { + const entries = Object.entries(fields); + const size = entries.reduce((acc, [, f]) => acc + f.size, 0); + + const encode = (obj: EncodeShape): Uint8Array => { + const buf = new Uint8Array(size); + const view = new DataView(buf.buffer); + let off = 0; + for (const [key, field] of entries) { + field.encode((obj as Record)[key], view, off); + off += field.size; + } + return buf; + }; + + const decode = (buffer: Uint8Array): DecodeShape => { + const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); + const result: Record = {}; + let off = 0; + for (const [key, field] of entries) { + result[key] = field.decode(view, off); + off += field.size; + } + return result as DecodeShape; + }; + + return { + size, + encode, + decode, + field: () => ({ + size, + encode: (val, view, off) => { + const encoded = encode(val); + new Uint8Array(view.buffer, view.byteOffset + off, size).set(encoded); + }, + decode: (view, off) => + decode(new Uint8Array(view.buffer, view.byteOffset + off, size)), + }), + }; +}; diff --git a/src/core/encryption/utils/keys.ts b/src/core/encryption/utils/keys.ts index 0fe6a29d6..9358adc76 100644 --- a/src/core/encryption/utils/keys.ts +++ b/src/core/encryption/utils/keys.ts @@ -1,108 +1,19 @@ -import { joinBuffers } from './buffers'; +import { CryptographyUtils } from '@core/features/encryption/worker/CryptographyUtils'; -/** - * Creates and return derived `CryptoKey` from a master password - */ -export async function getMasterKey( - masterPassword: string | ArrayBuffer, - salt: ArrayBuffer, -) { - const codec = new TextEncoder(); - - const key = - typeof masterPassword === 'string' - ? codec.encode(masterPassword) - : masterPassword; - - return await self.crypto.subtle - .importKey('raw', key, { name: 'PBKDF2' }, false, ['deriveBits', 'deriveKey']) - // We don't want to use key from original password, only derived keys, - // so here we derive a new key based on password - .then((key) => - self.crypto.subtle.deriveKey( - { - name: 'PBKDF2', - salt: new Uint8Array(salt), - iterations: 100000, - hash: 'SHA-512', - }, - key, - { - name: 'AES-GCM', - length: 256, - }, - - // Whether or not the key is extractable - true, - - // This usage limitations will be ignored, because we import and then export key with new permissions - ['encrypt', 'decrypt'], - ), - ) - // Export and import of key is necessary, to bypass limitation about key can't be derived of derived key - // Otherwise generates error `DOMException: key.algorithm does not match that of operation` - .then((key) => self.crypto.subtle.exportKey('raw', key)) - .then((key) => - self.crypto.subtle.importKey('raw', key, { name: 'PBKDF2' }, false, [ - 'deriveBits', - 'deriveKey', - ]), - ); -} - -type KeyAlgorithm = - | AlgorithmIdentifier - | AesDerivedKeyParams - | HmacImportParams - | HkdfParams - | Pbkdf2Params; +export const KEY_SALT_BYTES = 32; /** - * Creates object to generate derived keys and bytes based on provided master key + * We must never use an user password anywhere, because of potentially low entropy. + * We use Argon2id algorithm to derive bits and make a password brute force difficult */ -export async function getDerivedKeysManager( - masterKey: CryptoKey, +export const deriveBitsFromPassword = async ( + password: string, salt: Uint8Array, -) { - const codec = new TextEncoder(); - - const getScopedSalt = (scope: string) => - joinBuffers([codec.encode(scope).buffer, salt.buffer]); - - return { - async getDerivedKey(context: string, algorithm: KeyAlgorithm) { - return self.crypto.subtle.deriveKey( - { - name: 'PBKDF2', - salt: getScopedSalt(context), - iterations: 100000, - hash: 'SHA-512', - }, - masterKey, - algorithm, - - // Whether or not the key is extractable (less secure) or not (more secure) - // when false, the key can only be passed as a web crypto object, not inspected - true, - - // this web crypto object will only be allowed for these functions - ['encrypt', 'decrypt'], - ); - }, - - async getDerivedBytes(context: string, length: number) { - if (length % 8 !== 0) throw new TypeError('Length is not multiple to 8'); - - return self.crypto.subtle.deriveBits( - { - name: 'PBKDF2', - salt: getScopedSalt(context), - iterations: 100000, - hash: 'SHA-256', - }, - masterKey, - length, - ); - }, - }; -} +) => { + const utils = new CryptographyUtils(); + return utils + .deriveBits(new TextEncoder().encode(password), salt, 256) + .finally(async () => { + await utils.dispose(); + }); +}; diff --git a/src/core/encryption/utils/random.ts b/src/core/encryption/utils/random.ts index d350fad58..c13990683 100644 --- a/src/core/encryption/utils/random.ts +++ b/src/core/encryption/utils/random.ts @@ -1,26 +1,25 @@ /** * Fill buffer with cryptographically strong random values and return the same buffer */ -export function fillBufferWithRandomBytes(buffer: T): T { +export function fillBufferWithRandomBytes(buffer: Uint8Array): Uint8Array { const bytesLimit = 65535; // Fill if quote is not exceeded if (buffer.byteLength <= bytesLimit) { - self.crypto.getRandomValues(buffer); + globalThis.crypto.getRandomValues(buffer); return buffer; } // Fill buffer by blocks - const bufferView = new Uint8Array(buffer.buffer); for (let offset = 0; offset < buffer.byteLength; offset += bytesLimit) { // Generate block const bytesToFill = buffer.byteLength - offset; const bytesToAdd = Math.min(bytesLimit, bytesToFill); const blockBuffer = new Uint8Array(bytesToAdd); - self.crypto.getRandomValues(blockBuffer); + globalThis.crypto.getRandomValues(blockBuffer); // Fill with offset - bufferView.set(blockBuffer, offset); + buffer.set(blockBuffer, offset); } return buffer; @@ -31,7 +30,8 @@ export function fillBufferWithRandomBytes(buffer: T): * * @param bytesLength buffer size in bytes */ -export function getRandomBytes(bytesLength = 16): ArrayBuffer { - const typedArray = new Uint8Array(bytesLength); - return fillBufferWithRandomBytes(typedArray).buffer; +export function getRandomBytes(bytesLength = 16) { + return fillBufferWithRandomBytes( + new Uint8Array(bytesLength), + ) as Uint8Array; } diff --git a/src/core/encryption/utils/xor.ts b/src/core/encryption/utils/xor.ts index d7244a38c..8a60ff2d9 100644 --- a/src/core/encryption/utils/xor.ts +++ b/src/core/encryption/utils/xor.ts @@ -1,12 +1,32 @@ -export function xor(a: Uint8Array, b: Uint8Array) { - if (a.byteLength !== b.byteLength) - throw new TypeError('Buffers length are not equal'); +/* eslint-disable no-bitwise */ - const result = new Uint8Array(a.byteLength); - for (let offset = 0; offset < a.byteLength; offset += 1) { - // eslint-disable-next-line no-bitwise - result[offset] = a[offset] ^ b[offset]; - } +export function xor16( + r: Uint8Array, + a: Uint8Array, + b: Uint8Array, +): Uint8Array { + // This one is much slower + // a.forEach((x, index) => { + // r[index] = x ^ b[index] + // }) - return result; + // No length check, no loop, no view construction. + // 16 scalar XORs — V8 can keep all of these in registers. + r[0] = a[0] ^ b[0]; + r[1] = a[1] ^ b[1]; + r[2] = a[2] ^ b[2]; + r[3] = a[3] ^ b[3]; + r[4] = a[4] ^ b[4]; + r[5] = a[5] ^ b[5]; + r[6] = a[6] ^ b[6]; + r[7] = a[7] ^ b[7]; + r[8] = a[8] ^ b[8]; + r[9] = a[9] ^ b[9]; + r[10] = a[10] ^ b[10]; + r[11] = a[11] ^ b[11]; + r[12] = a[12] ^ b[12]; + r[13] = a[13] ^ b[13]; + r[14] = a[14] ^ b[14]; + r[15] = a[15] ^ b[15]; + return r; } diff --git a/src/core/features/encryption/algorithms.ts b/src/core/features/encryption/algorithms.ts index d6b7bc41d..c0431ba80 100644 --- a/src/core/features/encryption/algorithms.ts +++ b/src/core/features/encryption/algorithms.ts @@ -4,6 +4,31 @@ import { ENCRYPTION_ALGORITHM } from '.'; export const ENCRYPTION_ALGORITHM_OPTIONS = [ ENCRYPTION_ALGORITHM.AES, ENCRYPTION_ALGORITHM.TWOFISH, - formatAlgorithms([ENCRYPTION_ALGORITHM.AES, ENCRYPTION_ALGORITHM.TWOFISH]), - formatAlgorithms([ENCRYPTION_ALGORITHM.TWOFISH, ENCRYPTION_ALGORITHM.AES]), + ENCRYPTION_ALGORITHM.SERPENT, + ENCRYPTION_ALGORITHM.XChaCha20, + + formatAlgorithms([ENCRYPTION_ALGORITHM.XChaCha20, ENCRYPTION_ALGORITHM.AES]), + formatAlgorithms([ENCRYPTION_ALGORITHM.XChaCha20, ENCRYPTION_ALGORITHM.TWOFISH]), + + formatAlgorithms([ + ENCRYPTION_ALGORITHM.AES, + ENCRYPTION_ALGORITHM.TWOFISH, + ENCRYPTION_ALGORITHM.SERPENT, + ]), + + formatAlgorithms([ + ENCRYPTION_ALGORITHM.XChaCha20, + ENCRYPTION_ALGORITHM.AES, + ENCRYPTION_ALGORITHM.SERPENT, + ]), + formatAlgorithms([ + ENCRYPTION_ALGORITHM.XChaCha20, + ENCRYPTION_ALGORITHM.TWOFISH, + ENCRYPTION_ALGORITHM.SERPENT, + ]), + formatAlgorithms([ + ENCRYPTION_ALGORITHM.XChaCha20, + ENCRYPTION_ALGORITHM.AES, + ENCRYPTION_ALGORITHM.TWOFISH, + ]), ]; diff --git a/src/core/features/encryption/configureEncryptionPipeline.ts b/src/core/features/encryption/configureEncryptionPipeline.ts new file mode 100644 index 000000000..eeba7dd58 --- /dev/null +++ b/src/core/features/encryption/configureEncryptionPipeline.ts @@ -0,0 +1,80 @@ +import z from 'zod'; +import { AESCipher } from '@core/encryption/ciphers/AES'; +import { ensureWasmIsLoaded, SerpentCipher } from '@core/encryption/ciphers/Serpent'; +import { TwofishCTRCipher } from '@core/encryption/ciphers/Twofish'; +import { XChaCha20Cipher } from '@core/encryption/ciphers/XChaCha20'; +import { HKDFDerivedKeys } from '@core/encryption/utils/HKDFDerivedKeys'; + +import { EncryptionController } from '../../encryption/EncryptionController'; +import { BufferIntegrityProcessor } from '../../encryption/processors/BufferIntegrityProcessor'; +import { BufferSizeObfuscationProcessor } from '../../encryption/processors/BufferSizeObfuscationProcessor'; +import { PipelineProcessor } from '../../encryption/processors/PipelineProcessor'; +import { getRandomBytes } from '../../encryption/utils/random'; + +import { ENCRYPTION_ALGORITHM } from '.'; + +export const configureEncryptionPipeline = async ( + hkdf: HKDFDerivedKeys, + algorithm: string, +) => { + const cipherNames = z + .string() + .transform((s) => s.split('-')) + .pipe( + z + .enum(ENCRYPTION_ALGORITHM, { + error(info) { + return `Invalid algorithm ${info.input}`; + }, + }) + .array(), + ) + .parse(algorithm); + + const ciphers = await Promise.all( + cipherNames.map(async (cipherName, index) => { + switch (cipherName) { + case ENCRYPTION_ALGORITHM.AES: { + const key = await hkdf.deriveBits(256, String(index)); + return new AESCipher(new Uint8Array(key), getRandomBytes); + } + case ENCRYPTION_ALGORITHM.TWOFISH: { + const key = await hkdf.deriveBits(256, String(index)); + + const cipher = new TwofishCTRCipher( + new Uint8Array(key), + getRandomBytes, + ); + await cipher.load(); + return cipher; + } + case ENCRYPTION_ALGORITHM.SERPENT: { + const key = await hkdf.deriveBits(256, String(index)); + + await ensureWasmIsLoaded(); + return new SerpentCipher(new Uint8Array(key), getRandomBytes); + } + case ENCRYPTION_ALGORITHM.XChaCha20: { + const key = await hkdf.deriveBits(256, String(index)); + return new XChaCha20Cipher(new Uint8Array(key), getRandomBytes); + } + + default: { + throw new Error(`Unknown cipher ${cipherName}`); + } + } + }), + ); + + const hmacKey = await hkdf + .deriveBits(256, 'hmac') + .then((buffer) => new Uint8Array(buffer)); + + return new EncryptionController( + new PipelineProcessor([ + new BufferSizeObfuscationProcessor(getRandomBytes), + ...ciphers, + new BufferIntegrityProcessor(hmacKey), + ]), + ); +}; diff --git a/src/core/features/encryption/index.ts b/src/core/features/encryption/index.ts index 8d3a70f6f..4ce77222a 100644 --- a/src/core/features/encryption/index.ts +++ b/src/core/features/encryption/index.ts @@ -1,4 +1,6 @@ export enum ENCRYPTION_ALGORITHM { AES = 'AES', TWOFISH = 'Twofish', + SERPENT = 'Serpent', + XChaCha20 = 'XChaCha20', } diff --git a/src/core/features/encryption/worker/CryptographyUtils.test.ts b/src/core/features/encryption/worker/CryptographyUtils.test.ts new file mode 100644 index 000000000..817d76a08 --- /dev/null +++ b/src/core/features/encryption/worker/CryptographyUtils.test.ts @@ -0,0 +1,47 @@ +import { CryptographyUtils } from './CryptographyUtils'; + +test('Matches a derived bits snapshot', async () => { + const utils = new CryptographyUtils(); + onTestFinished(() => utils.dispose()); + + const key = new Uint8Array(100).fill(0xff); + expect(key).toEqual(new Uint8Array(100).fill(0xff)); + + await expect( + utils.deriveBits(key, new Uint8Array(16), 128), + ).resolves.toMatchSnapshot(); +}); + +test('Returns 32 bytes for a 256-bit request', async () => { + const utils = new CryptographyUtils(); + + const key = await utils.deriveBits( + new TextEncoder().encode('password'), + new Uint8Array(16), + 256, + ); + expect(key).toHaveLength(32); +}); + +test('Error must be thrown when requested bits length is not equal to a full byte', async () => { + const utils = new CryptographyUtils(); + await expect( + utils.deriveBits(new Uint8Array(100), new Uint8Array(16), 191), + ).rejects.toThrow('Length must be a whole number of bits'); +}); + +test('Error must be thrown when requested bits is less than 128', async () => { + const utils = new CryptographyUtils(); + await expect( + utils.deriveBits(new Uint8Array(100), new Uint8Array(16), 96), + ).rejects.toThrow('Invalid length. The minimal length is 128 bits'); +}); + +test('Errors must be thrown after dispose', async () => { + const utils = new CryptographyUtils(); + + await utils.dispose(); + await expect( + utils.deriveBits(new Uint8Array(100), new Uint8Array(16), 32), + ).rejects.toThrow(); +}); diff --git a/src/core/features/encryption/worker/CryptographyUtils.ts b/src/core/features/encryption/worker/CryptographyUtils.ts new file mode 100644 index 000000000..7e74d3b6b --- /dev/null +++ b/src/core/features/encryption/worker/CryptographyUtils.ts @@ -0,0 +1,47 @@ +import { Remote, wrap } from 'comlink'; + +import { CryptographyUtilsWorker } from '.'; + +export class CryptographyUtils implements CryptographyUtilsWorker { + private state: { + api: Remote; + worker: Worker; + } | null = null; + constructor() { + const worker = new Worker( + /* webpackChunkName: "CryptographyUtils.worker" */ new URL( + './CryptographyUtils.worker', + import.meta.url, + ), + { type: 'module' }, + ); + const api = wrap(worker); + + this.state = { api, worker }; + } + + private isDisposed = false; + public async dispose() { + if (!this.state) return; + + this.state.worker.terminate(); + this.isDisposed = true; + + this.state = null; + } + + private getState() { + if (this.isDisposed) throw new Error('Instance is disposed'); + if (!this.state) throw new Error('State is not set yet'); + + return this.state; + } + + public async deriveBits( + input: Uint8Array, + salt: Uint8Array, + length: number, + ) { + return this.getState().api.deriveBits(input, salt, length); + } +} diff --git a/src/core/features/encryption/worker/CryptographyUtils.worker.ts b/src/core/features/encryption/worker/CryptographyUtils.worker.ts new file mode 100644 index 000000000..ea48b20e6 --- /dev/null +++ b/src/core/features/encryption/worker/CryptographyUtils.worker.ts @@ -0,0 +1,52 @@ +import { Endpoint, expose, transfer } from 'comlink'; +import sodium from 'libsodium-wrappers-sumo'; + +import { CryptographyUtilsWorker } from '.'; + +expose( + { + async deriveBits(input, salt, length) { + try { + await sodium.ready; + + if (!Number.isInteger(length) || length % 8 !== 0) + throw new RangeError( + `Length must be a whole number of bits, got ${length}`, + ); + + if (length < 128) + throw new RangeError( + 'Invalid length. The minimal length is 128 bits', + ); + + const outputBytes = length / 8; + + if (salt.byteLength !== sodium.crypto_pwhash_argon2id_SALTBYTES) + throw new Error( + `Expected salt size is ${sodium.crypto_pwhash_argon2id_SALTBYTES} bytes`, + ); + + // 512mb + const memory = 1024 ** 2 * 512; + const ops = 2; + + const key = sodium.crypto_pwhash( + outputBytes, + input, + salt, + ops, + memory, + sodium.crypto_pwhash_ALG_ARGON2ID13, + ); + + // Ensure returned buffer is a `Uint8Array` + const buffer = key.slice(); + return transfer(buffer, [buffer.buffer]); + } finally { + sodium.memzero(input); + sodium.memzero(salt); + } + }, + } satisfies CryptographyUtilsWorker, + self as Endpoint, +); diff --git a/src/core/features/encryption/worker/Encryption.worker.ts b/src/core/features/encryption/worker/Encryption.worker.ts index 52e410ef5..d6d6675d4 100644 --- a/src/core/features/encryption/worker/Encryption.worker.ts +++ b/src/core/features/encryption/worker/Encryption.worker.ts @@ -1,17 +1,9 @@ import { Endpoint, expose, transfer } from 'comlink'; -import { IEncryptionProcessor } from '@core/encryption'; -import { AESGCMCipher } from '@core/encryption/ciphers/AES'; -import { TwofishCTRCipher } from '@core/encryption/ciphers/Twofish'; +import { HKDFDerivedKeys } from '@core/encryption/utils/HKDFDerivedKeys'; import { EncryptionController } from '../../../encryption/EncryptionController'; -import { BufferIntegrityProcessor } from '../../../encryption/processors/BufferIntegrityProcessor'; -import { BufferSizeObfuscationProcessor } from '../../../encryption/processors/BufferSizeObfuscationProcessor'; -import { PipelineProcessor } from '../../../encryption/processors/PipelineProcessor'; -import { getDerivedKeysManager, getMasterKey } from '../../../encryption/utils/keys'; -import { getRandomBytes } from '../../../encryption/utils/random'; -import { parseAlgorithms } from '../utils'; -import { ENCRYPTION_ALGORITHM } from '..'; +import { configureEncryptionPipeline } from '../configureEncryptionPipeline'; import { EncryptionWorker } from '.'; const workerId = Date.now(); @@ -24,40 +16,9 @@ expose( self.setInterval(() => console.debug('Worker pulse', workerId), 1000); } - const derivedKeys = await getMasterKey(key, salt).then((masterKey) => - getDerivedKeysManager(masterKey, new Uint8Array(salt)), - ); - - const cipherMap: Record< - ENCRYPTION_ALGORITHM, - () => Promise - > = { - [ENCRYPTION_ALGORITHM.AES]: async () => { - const key = await derivedKeys.getDerivedKey('aes-gcm-cipher', { - name: 'AES-GCM', - length: 256, - }); - return new AESGCMCipher(key, getRandomBytes); - }, - [ENCRYPTION_ALGORITHM.TWOFISH]: async () => { - const key = await derivedKeys.getDerivedBytes( - 'twofish-ctr-cipher', - 256, - ); - return new TwofishCTRCipher(new Uint8Array(key), getRandomBytes); - }, - }; - - const ciphers = await Promise.all( - parseAlgorithms(algorithm).map((name) => cipherMap[name]()), - ); - - encryptionController = new EncryptionController( - new PipelineProcessor([ - new BufferIntegrityProcessor(), - new BufferSizeObfuscationProcessor(getRandomBytes), - ...ciphers, - ]), + encryptionController = await configureEncryptionPipeline( + new HKDFDerivedKeys(key, salt), + algorithm, ); }, diff --git a/src/core/features/encryption/worker/__snapshots__/CryptographyUtils.test.ts.snap b/src/core/features/encryption/worker/__snapshots__/CryptographyUtils.test.ts.snap new file mode 100644 index 000000000..f414b4aca --- /dev/null +++ b/src/core/features/encryption/worker/__snapshots__/CryptographyUtils.test.ts.snap @@ -0,0 +1,22 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Matches a derived bits snapshot 1`] = ` +Uint8Array [ + 113, + 157, + 213, + 169, + 126, + 189, + 88, + 205, + 140, + 34, + 75, + 212, + 192, + 190, + 108, + 96, +] +`; diff --git a/src/core/features/encryption/worker/encryption.bench.ts b/src/core/features/encryption/worker/encryption.bench.ts index 2000886e7..10a810eb7 100644 --- a/src/core/features/encryption/worker/encryption.bench.ts +++ b/src/core/features/encryption/worker/encryption.bench.ts @@ -1,4 +1,5 @@ import { bench } from 'vitest'; +import { getRandomBytes } from '@core/encryption/utils/random'; import { ENCRYPTION_ALGORITHM_OPTIONS } from '../algorithms'; import { WorkerEncryptionProcessor } from './WorkerEncryptionProcessor'; @@ -14,10 +15,11 @@ describe(`Encrypt 1k chars`, () => { ); }, { + throws: true, async setup() { processor = new WorkerEncryptionProcessor({ - key: 'secret password', - salt: new ArrayBuffer(100), + key: getRandomBytes(32), + salt: getRandomBytes(100), algorithm, disablePulse: true, }); @@ -45,8 +47,8 @@ describe(`Encrypt 10k chars`, () => { { async setup() { processor = new WorkerEncryptionProcessor({ - key: 'secret password', - salt: new ArrayBuffer(100), + key: getRandomBytes(32), + salt: getRandomBytes(100), algorithm, disablePulse: true, }); @@ -74,8 +76,8 @@ describe(`Encrypt 100k chars`, () => { { async setup() { processor = new WorkerEncryptionProcessor({ - key: 'secret password', - salt: new ArrayBuffer(100), + key: getRandomBytes(32), + salt: getRandomBytes(100), algorithm, disablePulse: true, }); @@ -101,10 +103,11 @@ describe(`Encrypt 1m chars`, () => { ); }, { + throws: true, async setup() { processor = new WorkerEncryptionProcessor({ - key: 'secret password', - salt: new ArrayBuffer(100), + key: getRandomBytes(32), + salt: getRandomBytes(100), algorithm, disablePulse: true, }); @@ -118,3 +121,35 @@ describe(`Encrypt 1m chars`, () => { ); }); }); + +describe(`Decrypt 1mb data`, () => { + const testData = getRandomBytes(1024 ** 2).buffer; + ENCRYPTION_ALGORITHM_OPTIONS.map((algorithm) => { + let processor: WorkerEncryptionProcessor; + let data: ArrayBuffer; + + bench( + algorithm, + async () => { + await processor.decrypt(data); + }, + { + throws: true, + async setup() { + processor = new WorkerEncryptionProcessor({ + key: getRandomBytes(32), + salt: getRandomBytes(100), + algorithm, + disablePulse: true, + }); + await processor.load(); + data = await processor.encrypt(testData.slice()); + }, + async teardown() { + await processor.terminate(); + }, + iterations: 10, + }, + ); + }); +}); diff --git a/src/core/features/encryption/worker/index.ts b/src/core/features/encryption/worker/index.ts index 0cacf2dac..ce641b213 100644 --- a/src/core/features/encryption/worker/index.ts +++ b/src/core/features/encryption/worker/index.ts @@ -1,14 +1,20 @@ import { IEncryptionController } from '@core/encryption'; export type EncryptionConfig = { - // TODO: make API stronger, accept only `ArrayBuffer` - // and rewrite its content after a key derivation - key: string | ArrayBuffer; - salt: ArrayBuffer; algorithm: string; + key: Uint8Array; + salt: Uint8Array; }; export type EncryptionWorkerConfig = EncryptionConfig & { disablePulse?: boolean }; export interface EncryptionWorker extends IEncryptionController { init(config: EncryptionWorkerConfig): Promise; } + +export interface CryptographyUtilsWorker { + deriveBits( + input: Uint8Array, + salt: Uint8Array, + length: number, + ): Promise>; +} diff --git a/src/core/features/files/FilesStorage.test.ts b/src/core/features/files/FilesStorage.test.ts index 52e0d0eec..be6ec9ffc 100644 --- a/src/core/features/files/FilesStorage.test.ts +++ b/src/core/features/files/FilesStorage.test.ts @@ -1,7 +1,7 @@ import { webcrypto } from 'crypto'; -import { AESGCMCipher } from '@core/encryption/ciphers/AES'; +import { createFakeRandomBytesGenerator } from '@core/encryption/__tests__/random'; +import { AESCipher } from '@core/encryption/ciphers/AES'; import { EncryptionController } from '@core/encryption/EncryptionController'; -import { getDerivedKeysManager, getMasterKey } from '@core/encryption/utils/keys'; import { EncryptedFS } from './EncryptedFS'; import { InMemoryFS } from './InMemoryFS'; @@ -37,30 +37,14 @@ const systems: { name: string; init(): Promise }[] = [ { name: 'EncryptedFS', async init() { - const derivedKeys = await getMasterKey( - webcrypto.getRandomValues(new Uint8Array(new ArrayBuffer(300))).buffer, - webcrypto.getRandomValues(new Uint8Array(new ArrayBuffer(300))).buffer, - ).then((masterKey) => - getDerivedKeysManager( - masterKey, - webcrypto.getRandomValues(new Uint8Array(new ArrayBuffer(300))), - ), - ); - - const aes = await derivedKeys.getDerivedKey('aes-gcm-cipher', { - name: 'AES-GCM', - length: 256, - }); - - const getRandomBytesMock = (length = 16) => - new Uint8Array(length).map( - (_, idx) => idx + Math.max(0, idx + ((length + idx) % 255)), - ).buffer; + const seededRandomBytes = createFakeRandomBytesGenerator(0); const fs = new InMemoryFS(); return new EncryptedFS( fs, - new EncryptionController(new AESGCMCipher(aes, getRandomBytesMock)), + new EncryptionController( + new AESCipher(seededRandomBytes(32), seededRandomBytes), + ), ); }, }, diff --git a/src/features/App/Profiles/hooks/useProfileContainers.ts b/src/features/App/Profiles/hooks/useProfileContainers.ts index 16091f91b..a80b32cc8 100644 --- a/src/features/App/Profiles/hooks/useProfileContainers.ts +++ b/src/features/App/Profiles/hooks/useProfileContainers.ts @@ -6,6 +6,7 @@ import { openSQLite } from '@core/database/sqlite/openSQLite'; import { EncryptionController } from '@core/encryption/EncryptionController'; import { PlaceholderEncryptionController } from '@core/encryption/PlaceholderEncryptionController'; import { base64ToBytes } from '@core/encryption/utils/encoding'; +import { deriveBitsFromPassword, KEY_SALT_BYTES } from '@core/encryption/utils/keys'; import { createEncryption } from '@core/features/encryption/createEncryption'; import { IFilesStorage } from '@core/features/files'; import { EncryptedFS } from '@core/features/files/EncryptedFS'; @@ -33,18 +34,26 @@ const decryptKey = async ({ }: { encryptedKey: ArrayBuffer; password: string; - salt: ArrayBuffer; + salt: Uint8Array; algorithm: string; }) => { - const encryption = await createEncryption({ key: password, salt, algorithm }); + const keyPassword = await deriveBitsFromPassword(password, salt); + const encryption = await createEncryption({ + key: keyPassword, + salt: new Uint8Array(encryptedKey).slice(0, KEY_SALT_BYTES), + algorithm, + }); + return encryption .getContent() - .decrypt(encryptedKey) + .decrypt(encryptedKey.slice(KEY_SALT_BYTES)) .finally(() => { encryption.dispose(); - }); + }) + .then((buffer) => new Uint8Array(buffer)); }; +// TODO: cover with tests to ensure we can decrypt exists vault /** * Hook to manage active and opened profiles */ @@ -84,8 +93,7 @@ export const useProfileContainers = () => { throw new Error('Key file is not found in profile directory'); } - const salt = new Uint8Array(base64ToBytes(profile.encryption.salt)) - .buffer; + const salt = new Uint8Array(base64ToBytes(profile.encryption.salt)); const key = await decryptKey({ encryptedKey: encryptedKeyBuffer, password: password, @@ -95,7 +103,7 @@ export const useProfileContainers = () => { const encryption = await createEncryption({ key, - salt, + salt: new Uint8Array(encryptedKeyBuffer).slice(KEY_SALT_BYTES), algorithm: profile.encryption.algorithm, }); diff --git a/src/features/App/WorkspaceManager/ProfileCreator/index.tsx b/src/features/App/WorkspaceManager/ProfileCreator/index.tsx index 94a3fac9b..a009d664c 100644 --- a/src/features/App/WorkspaceManager/ProfileCreator/index.tsx +++ b/src/features/App/WorkspaceManager/ProfileCreator/index.tsx @@ -16,6 +16,7 @@ import { useDisclosure, VStack, } from '@chakra-ui/react'; +import { ENCRYPTION_ALGORITHM } from '@core/features/encryption'; import { ENCRYPTION_ALGORITHM_OPTIONS } from '@core/features/encryption/algorithms'; import { TELEMETRY_EVENT_NAME } from '@core/features/telemetry'; import { useTelemetryTracker } from '@features/telemetry'; @@ -232,7 +233,9 @@ export const ProfileCreator: FC = ({