├── .github └── workflows │ ├── node.js.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── __tests__ │ ├── picl_vectors.test.ts │ ├── rfc_5054.test.ts │ └── srp.test.ts ├── bigInt.ts ├── crypt │ ├── __tests__ │ │ └── crypt.test.ts │ ├── base64.ts │ ├── box.ts │ ├── crypt.ts │ ├── hkdf.ts │ ├── index.ts │ └── sealedbox.ts ├── index.ts ├── params.ts └── srp.ts ├── tsconfig.json └── vite.config.ts /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | name: Run Tests 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: [16.x] 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | cache: 'npm' 23 | - run: npm ci 24 | - run: npm test 25 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: NPM Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | packages: write 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | node-version: 18 | - 16 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@main 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | registry-url: 'https://registry.npmjs.org' 27 | - name: Install 28 | run: npm ci 29 | - name: Build 30 | run: npm run build 31 | - name: Publish Package 32 | run: npm publish --access public 33 | env: 34 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vim and emacs ancillary files 2 | *~ 3 | *.swp 4 | *.swo 5 | 6 | # other stuff 7 | .DS_Store 8 | 9 | # locally installed node modules 10 | node_modules 11 | .gitmodules 12 | 13 | # awsbox setup notes 14 | awsbox_notes.txt 15 | 16 | # IntelliJ 17 | .idea 18 | 19 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2013 Jed Parsons 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Node.js CI](https://github.com/Kong/srp-js/actions/workflows/node.js.yml/badge.svg)](https://github.com/Kong/srp-js/actions/workflows/node.js.yml) 2 | 3 | # SRP 4 | 5 | This is a fork of [node-srp](https://github.com/mozilla/node-srp) with 6 | `bignum` replaced with `jsbn` to make it easier to build for browser 7 | environments. 8 | 9 | ## Licence 10 | 11 | MIT 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@getinsomnia/srp-js", 3 | "description": "Secure Remote Password (SRP)", 4 | "version": "0.3.0", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "types": "dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/index.js" 11 | } 12 | }, 13 | "scripts": { 14 | "test": "vitest", 15 | "build": "vite build && npm run tsc", 16 | "tsc": "tsc --emitDeclarationOnly --declarationDir dist" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/getinsomnia/srp-js" 21 | }, 22 | "author": "Kong ", 23 | "licence": "MIT", 24 | "readmeFilename": "README.md", 25 | "dependencies": { 26 | "@types/node-forge": "^1.3.1", 27 | "blakejs": "^1.2.1", 28 | "jsbn": "^1.1.0", 29 | "node-forge": "^1.3.1", 30 | "tweetnacl": "^1.0.3" 31 | }, 32 | "devDependencies": { 33 | "@types/jsbn": "^1.2.30", 34 | "typescript": "^4.9.5", 35 | "vite": "^4.1.1", 36 | "vite-plugin-node-polyfills": "^0.7.0", 37 | "vitest": "^0.28.4" 38 | }, 39 | "files": [ 40 | "dist" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/__tests__/picl_vectors.test.ts: -------------------------------------------------------------------------------- 1 | import { BigInteger } from "../bigInt"; 2 | import { Client, computeVerifier, params as _params, Server } from ".."; 3 | import { describe, it, assert } from "vitest"; 4 | import { Params } from "../params"; 5 | /* 6 | * Vectors from https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol 7 | * 8 | * Verify that we are inter-compatible with the SRP implementation used by 9 | * Mozilla's Identity-Attached Services, aka PiCl (Profile in the Cloud). 10 | * 11 | * Note that P is the HKDF-stretched key, computed elsewhere. 12 | */ 13 | 14 | function join(s: string) { 15 | return s.split(/\s/).join(""); 16 | } 17 | function decimal(s: string) { 18 | return new BigInteger(join(s), 10).toBuffer(); 19 | } 20 | function h(s: string) { 21 | return Buffer.from(join(s), "hex"); 22 | } 23 | 24 | const params = _params["2048"]; 25 | 26 | /* inputs_1/expected_1 are the main PiCl test vectors. They were mechanically 27 | * generated to force certain derived values (stretched-password "P", v, A, 28 | * B, and S) to begin with a 0x00 byte (to exercise padding bugs). 29 | */ 30 | 31 | const inputs_1 = { 32 | I: Buffer.from("andré@example.org", "utf8"), 33 | P: h("00f9b71800ab5337 d51177d8fbc682a3 653fa6dae5b87628 eeec43a18af59a9d"), 34 | salt: h("00f1000000000000000000000000000000000000000000000000000000000179"), 35 | // a and b are usually random. For testing, we force them to specific values. 36 | a: h( 37 | " 00f2000000000000 0000000000000000 0000000000000000 0000000000000000" + 38 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 39 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 40 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 41 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 42 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 43 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 44 | "0000000000000000 0000000000000000 0000000000000000 000000000000d3d7" 45 | ), 46 | b: h( 47 | " 00f3000000000000 0000000000000000 0000000000000000 0000000000000000" + 48 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 49 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 50 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 51 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 52 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 53 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 54 | "0000000000000000 0000000000000000 0000000000000000 000000000000000f" 55 | ) 56 | }; 57 | 58 | const expected_1 = { 59 | // 'k' encodes the group (N and g), used in SRP-6a 60 | k: decimal( 61 | "2590038599070950300691544216303772122846747035652616593381637186118123578112" 62 | ), 63 | // 'x' is derived from the salt and password 64 | // 'v' is the SRP verifier 65 | x: h("b5200337cc3f3f92 6cdddae0b2d31029 c069936a844aff58 779a545be89d0abe"), 66 | v: h( 67 | " 00173ffa0263e63c cfd6791b8ee2a40f 048ec94cd95aa8a3 125726f9805e0c82" + 68 | "83c658dc0b607fbb 25db68e68e93f265 8483049c68af7e82 14c49fde2712a775" + 69 | "b63e545160d64b00 189a86708c69657d a7a1678eda0cd79f 86b8560ebdb1ffc2" + 70 | "21db360eab901d64 3a75bf1205070a57 91230ae56466b8c3 c1eb656e19b794f1" + 71 | "ea0d2a077b3a7553 50208ea0118fec8c 4b2ec344a05c66ae 1449b32609ca7189" + 72 | "451c259d65bd15b3 4d8729afdb5faff8 af1f3437bbdc0c3d 0b069a8ab2a959c9" + 73 | "0c5a43d42082c774 90f3afcc10ef5648 625c0605cdaace6c 6fdc9e9a7e6635d6" + 74 | "19f50af773452247 0502cab26a52a198 f5b00a2798589165 07b0b4e9ef9524d6" 75 | ), 76 | // 'B' is the server's public message 77 | B: h( 78 | " 0022ce5a7b9d8127 7172caa20b0f1efb 4643b3becc535664 73959b07b790d3c3" + 79 | "f08650d5531c19ad 30ebb67bdb481d1d 9cf61bf272f84398 48fdda58a4e6abc5" + 80 | "abb2ac496da5098d 5cbf90e29b4b110e 4e2c033c70af7392 5fa37457ee13ea3e" + 81 | "8fde4ab516dff1c2 ae8e57a6b264fb9d b637eeeae9b5e43d faba9b329d3b8770" + 82 | "ce89888709e02627 0e474eef822436e6 397562f284778673 a1a7bc12b6883d1c" + 83 | "21fbc27ffb3dbeb8 5efda279a69a1941 4969113f10451603 065f0a0126666456" + 84 | "51dde44a52f4d8de 113e2131321df1bf 4369d2585364f9e5 36c39a4dce33221b" + 85 | "e57d50ddccb4384e 3612bbfd03a268a3 6e4f7e01de651401 e108cc247db50392" 86 | ), 87 | // 'A' is the client's public message 88 | A: h( 89 | " 007da76cb7e77af5 ab61f334dbd5a958 513afcdf0f47ab99 271fc5f7860fe213" + 90 | "2e5802ca79d2e5c0 64bb80a38ee08771 c98a937696698d87 8d78571568c98a1c" + 91 | "40cc6e7cb101988a 2f9ba3d65679027d 4d9068cb8aad6ebf f0101bab6d52b5fd" + 92 | "fa81d2ed48bba119 d4ecdb7f3f478bd2 36d5749f2275e948 4f2d0a9259d05e49" + 93 | "d78a23dd26c60bfb a04fd346e5146469 a8c3f010a627be81 c58ded1caaef2363" + 94 | "635a45f97ca0d895 cc92ace1d09a99d6 beb6b0dc0829535c 857a419e834db128" + 95 | "64cd6ee8a843563b 0240520ff0195735 cd9d316842d5d3f8 ef7209a0bb4b54ad" + 96 | "7374d73e79be2c39 75632de562c59647 0bb27bad79c3e2fc ddf194e1666cb9fc" 97 | ), 98 | // 'u' combines the two public messages 99 | u: h("b284aa1064e87751 50da6b5e2147b47c a7df505bed94a6f4 bb2ad873332ad732"), 100 | // 'S' is the shared secret 101 | S: h( 102 | " 0092aaf0f527906a a5e8601f5d707907 a03137e1b601e04b 5a1deb02a981f4be" + 103 | "037b39829a27dba5 0f1b27545ff2e287 29c2b79dcbdd32c9 d6b20d340affab91" + 104 | "a626a8075806c26f e39df91d0ad979f9 b2ee8aad1bc783e7 097407b63bfe58d9" + 105 | "118b9b0b2a7c5c4c debaf8e9a460f4bf 6247b0da34b760a5 9fac891757ddedca" + 106 | "f08eed823b090586 c63009b2d740cc9f 5397be89a2c32cdc fe6d6251ce11e44e" + 107 | "6ecbdd9b6d93f30e 90896d2527564c7e b9ff70aa91acc0ba c1740a11cd184ffb" + 108 | "989554ab58117c21 96b353d70c356160 100ef5f4c28d19f6 e59ea2508e8e8aac" + 109 | "6001497c27f362ed bafb25e0f045bfdf 9fb02db9c908f103 40a639fe84c31b27" 110 | ), 111 | // 'K' is the shared derived key 112 | K: h("e68fd0112bfa31dc ffc8e9c96a1cbadb 4c3145978ff35c73 e5bf8d30bbc7499a"), 113 | // 'M1' is the client's proof that it knows the shared key 114 | M1: h("27949ec1e0f16256 33436865edb037e2 3eb6bf5cb91873f2 a2729373c2039008") 115 | }; 116 | 117 | /* inputs_2/expected_2 have leading 0x00 bytes in 'x' and 'u' */ 118 | const inputs_2 = { 119 | I: inputs_1.I, 120 | P: inputs_1.P, 121 | salt: h("00f1000000000000000000000000000000000000000000000000000000000021"), 122 | a: h( 123 | " 00f2000000000000 0000000000000000 0000000000000000 0000000000000000" + 124 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 125 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 126 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 127 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 128 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 129 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 130 | "0000000000000000 0000000000000000 0000000000000000 000000000000000d" 131 | ), 132 | b: h( 133 | " 00f3000000000000 0000000000000000 0000000000000000 0000000000000000" + 134 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 135 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 136 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 137 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 138 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 139 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 140 | "0000000000000000 0000000000000000 0000000000000000 0000000000000001" 141 | ) 142 | }; 143 | const expected_2 = { 144 | k: expected_1.k, 145 | x: h("009b2740fb49284d 69cab7c916d449ee d7dcabf41332b8b8 d6928f529bd1a94e"), 146 | v: h( 147 | " 1cd8b856685672ee 7a5895d897121234 6c17c3472f2696e4 8cdeec5533c06693" + 148 | "179bc24802b762bc c1e1f8fc8abe607a f2f44aac9172e7dd 0c0110e45cf3b700" + 149 | "f8db153b67fb0e76 3c6710b8c1c26baf c3b67a50652ee0d7 c6045a5c4b51ff33" + 150 | "d0135065dca5d6bb 7e150e07414bd572 a954471059c1b466 d0530b0a80bd2d0c" + 151 | "f1bedf5abfc05c3c f2736ac40b083dcf 62271e834042ecb0 d4882ddd35403c1e" + 152 | "d24bc4ffe274c5f6 be50ec9b85aa0cfa 26d97e086ec45e06 3c29174d3dbe5490" + 153 | "1d2a557b7eb46b18 9e17cc721fc098a0 baee2f364a2b409d 49d9372a9625db11" + 154 | "acfd74ba7f41285f 9c1916d3caaf5238 852694bbde2a13f7 8fcc92d16658dd04" 155 | ), 156 | // 'B' is the server's public message 157 | B: h( 158 | " 485d56912c60d9c1 7af15494d4d50006 45eefa2d41f6bcb5 785e08efad0833a1" + 159 | "3cb43ee3869e78d4 c2006f42b9741782 a85c90a110cc9a74 4fc2a361d5535966" + 160 | "2dc5fa4a8d0c7c0e 63e0cf32a28af655 863dd5d66f550557 eacd3e3e64d90f9f" + 161 | "0d757403c9bbfb08 fcc9a35e1cb421d7 3bb93fa72d5b54ed bfa219d3867255ba" + 162 | "f96223eef038f085 722b2d14457a5a13 1857a56e66d3011b b5aa7504c4b9a346" + 163 | "8d0ebdd817d20105 be06ba261ea16740 723faa097f27ddc2 efe34cf8fe59451a" + 164 | "5bb3987d7161085f b8fc28d5cc28c466 6a3ca486ad0ca83d 1984248ac838574e" + 165 | "348fb9745ffd1163 f53b5566768a8971 237065d8f6e786be e15107125fb10df1" 166 | ), 167 | // 'A' is the client's public message 168 | A: h( 169 | " a4b17836b1e7d6f1 5b9901f644bcdf5e 119e7a861c6ee88d 006d8420a5066f22" + 170 | "d9bf5ccf3d380437 0d29d778ec40afcf c88de7bf22ec03fc 6ab12e0dd95d15e3" + 171 | "a6249c94393435b0 0d23b1b0439dabed cce1726b2b3cdea2 647c8790d604d87d" + 172 | "2ac890cfceec0dbe 434f09a9bc11d984 a1e1990f69956ae0 db6068992ad1715f" + 173 | "b4381516da83637a 73de4211908c8f2f f8b3a09e8535acf3 c2b8de4e9a632f89" + 174 | "9bfa08cee543b4ea 50d0aca0b3e4fbfa e49ffa2a1ab89a42 8bea928868828501" + 175 | "2e8af13fcdd444ad da9ad1d0ab4c2069 91919e5391bd2b1a ab8c2d006baceaf8" + 176 | "cdcb555a6b16e844 5b03e09776eba841 7576dac458afbbd5 2902dfb0282bed79" 177 | ), 178 | // 'u' combines the two public messages 179 | u: h("000e4039be3989ad 088dc17d8ade899a 6409e7e57b3e8518 cee1cbc77e1de243"), 180 | // 'S' is the shared secret 181 | S: h( 182 | " 5c7f591d134d19f9 fcedc2b4e3eecd3d 5deadfe7dd42bd59 b1c960516c65ab61" + 183 | "d007f8134e0a7ca3 0dd409128ef2c780 6784afd95985c8f3 c2d42cd73d26d315" + 184 | "541645d28aefabc9 980c9a6e5714b178 aa69e5321828ca00 f3d10d742776cfe4" + 185 | "4b7f5f5c0247addc 0ab0640b49b540ff 9bccea8702e1f996 49448680c00fb484" + 186 | "51919224d44236ba 1b1e5cf62a5946bd 637f189ff7b8eba9 7b719f18ad9251f0" + 187 | "a81c157604065388 d7bf4abbf774bfb2 d7b95ed8359b0d70 6ff5df0223992c81" + 188 | "4aac506e1bace002 d134ed5e41d74f93 a8f410dfe7dc5954 f70b6bafcd0ddfde" + 189 | "e75f0058f718ec14 f9bbeb29ff966e00 ddfdd2d38a1c7a68 ac455a57b972d528" 190 | ), 191 | // 'K' is the shared derived key 192 | K: h("b637ede0b7a31c46 b2567e855eb8a7f7 a994937deee76479 62afbe35d6929709"), 193 | // 'M1' is the client's proof that it knows the shared key 194 | M1: h("67c83797eb1a3987 e2d48d287e3bd772 d25db2b3cd86ea22 c8cf3ae932a1e45b") 195 | }; 196 | 197 | /* inputs_3/expected_3 have leading 0x00 bytes in 'x' and 'K' */ 198 | const inputs_3 = { 199 | I: inputs_2.I, 200 | P: inputs_2.P, 201 | salt: h("00f1000000000000000000000000000000000000000000000000000000000021"), 202 | a: h( 203 | " 00f2000000000000 0000000000000000 0000000000000000 0000000000000000" + 204 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 205 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 206 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 207 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 208 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 209 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 210 | "0000000000000000 0000000000000000 0000000000000000 00000000000001a0" 211 | ), 212 | b: inputs_2.b 213 | }; 214 | const expected_3 = { 215 | k: expected_2.k, 216 | x: expected_2.x, 217 | v: expected_2.v, 218 | B: expected_2.B, 219 | A: h( 220 | " 87b6da9e4162843b 4d5ee60c403ae3e1 e9fdab64883f13ab 4a44b0718a9ea1b6" + 221 | "1ad17c675e0f0395 b37d58a046a2d5ab 1fb665a9777abe80 8077ccf6fd8ec583" + 222 | "854eab98deb257d9 10e5bf5cafed4955 2a5cd9927c0979f7 5a21654644000173" + 223 | "aef6f2244296439c 10b3c61a03e7146e f6c9c9564b1d2bf5 1ece84d115965f9c" + 224 | "c82006bdb7a124da 3304bcc24c8f3724 522b748fb19a0cb6 b60e355acbf649b5" + 225 | "40b4972e24077c29 32004a3ad9e59464 2e90a3bfc8de7085 f4a4efc195bd06c9" + 226 | "6c7011f3c979eaab 469f06465a5b7239 afaee535aedc5bd2 1a220546e0e6b70b" + 227 | "5b6f54db3fea46d5 7ebc7fe46156d793 c59e6290d3cf9bc2 4316528da34f4640" 228 | ), 229 | u: h("865d0efca6cf17d6 f489e129231f1a48 b20c83ec6581d11f 3a2fa48ea93cd305"), 230 | S: h( 231 | " 0ae26456e1a0dec1 ce162fb2e5bc7300 3c285e17c0b44f03 7ebbc57f8020ceae" + 232 | "5d10a9e6e44eab2a 6915b582ab5f6e7d 16002ce05e524015 e9bc7c56d5131da4" + 233 | "d4c4d7c3debaffcd b60e58468bd2c0da 5de95855480190a3 5258c79032001882" + 234 | "3d836ca91848c5b6 3ca4265c3329eb44 161af9ce64cf4468 ef0eb88a788a0d07" + 235 | "52a69821278c94ae 7193161b5c638b55 bf732e2a5996ccc5 16335f9f3d00dfa9" + 236 | "8ac1b1e4971c5417 d34eba1e2a90ed60 a07d1d8be5b9d773 d8f2cb03bfb75994" + 237 | "249f7734081aa42d 58dd54f8f725b245 175cf7d102e1086c eba4cfe7e49a2d27" + 238 | "ffd6aef7549d402f bfcea78b4f3398ac 9ab1ee199f70acb6 4d2a17e159ff500d" 239 | ), 240 | K: h("00217598a4008956 4b17196bd43422d6 03a0a88a545b61b3 98c42c9cbcc1d1b3"), 241 | M1: h("96d815ecece1dff4 254cd77517b37b97 65e741c1a57169ab af538e867444ec7f") 242 | }; 243 | 244 | /* inputs_4/expected_4 have leading 0x00 bytes in 'x' and 'M1' */ 245 | const inputs_4 = { 246 | I: inputs_2.I, 247 | P: inputs_2.P, 248 | salt: h("00f1000000000000000000000000000000000000000000000000000000000021"), 249 | a: h( 250 | " 00f2000000000000 0000000000000000 0000000000000000 0000000000000000" + 251 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 252 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 253 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 254 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 255 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 256 | "0000000000000000 0000000000000000 0000000000000000 0000000000000000" + 257 | "0000000000000000 0000000000000000 0000000000000000 0000000000000190" 258 | ), 259 | b: inputs_2.b 260 | }; 261 | const expected_4 = { 262 | k: expected_2.k, 263 | x: expected_2.x, 264 | v: expected_2.v, 265 | B: expected_2.B, 266 | A: h( 267 | " 4aee66beefb92d12 c8e341814809afcd 9ce083c11abcda70 0c03d5379c429cb9" + 268 | "acbde6bb42a628f3 7a2536c864c40f74 f48a9d9356029a8b fe0e10cb9cf5a8a4" + 269 | "2e591841f426d281 edf7c9b04112d8ef bf73f9768a4faace ddd351d3e9380bf1" + 270 | "dcd0590c7ab50a95 bd23e9617e303bea 6f8fbe8a657b6417 4b60cdf5c059ba67" + 271 | "1b6735324ae0c30a e7f3e361de8f273c af7b2513fa048ed1 0106c66ce460c5cc" + 272 | "78544c790f5ffcce 378b79d5f02ec361 3a457b03fa0cc39c 80d6fdd645e24f65" + 273 | "c690f9478d5b331d c00eef68670edbf3 629fd1a6c85267d2 cbb90f1670e7ba09" + 274 | "cf2b5a9b00be8e11 f33e47a1c1f04eca f35bccb61af1116e 4d0f9d475017bad2" 275 | ), 276 | u: h("d0913eb75b61e15a 87756ffa04d4f967 e492bd0b330a2b11 fe8976aada2bb1ee"), 277 | S: h( 278 | " 7ba3ce4a3d236b95 3c2d0fee42195c85 081664a44f55b82d a3abf66ac68bdbd7" + 279 | "ad82d5ad95090782 5241fb706de8fc58 0a29e4579fbbedf3 0bec0138b3f76e06" + 280 | "f9c86b16ad673890 3003ce8c86cb14ea 552db904a20970a9 7d9258a768087d30" + 281 | "47a6e77520d32968 de3f64e94cd8c463 92c13e194194745c 8e53a9bb15a79473" + 282 | "2a645068970fcdd9 a7c98b4aec19773a 5196802c2e932e71 d3a4a340e6f4fe16" + 283 | "9e7ccc687f7246fe 20edeaf88d1125da c812751317f7213c d84f9efe2313d701" + 284 | "d4a9bf0242bfe703 26fc19b68c90e83b 59b5cc21886ab602 f8bfa16fb50c3147" + 285 | "9aad5e31698abf67 863b7ca6b6ac25a7 09a24d8f94c80bbf 691e38c81beb3c72" 286 | ), 287 | K: h("bd2a167a93b8496e 68c7e24b37956924 672eb8249d25c281 13984912d5cf27a6"), 288 | M1: h("00cef66a047d506c bf941c236218e583 5343534ae08cf0cd 0fb7980bed242e05") 289 | }; 290 | 291 | function hexequal(a: Buffer, b: Buffer, msg: string) { 292 | assert.equal(a.length, b.length, msg); 293 | assert.equal(a.toString("hex"), b.toString("hex"), msg); 294 | } 295 | 296 | function numequal(a: BigInteger, b: BigInteger, msg: string) { 297 | assert(a.eq(b), msg); 298 | } 299 | 300 | function checkVectors(params: Params, inputs: { 301 | I: Buffer; 302 | P: Buffer; 303 | salt: Buffer; 304 | a: Buffer; 305 | b: Buffer; 306 | }, expected: { 307 | k: Buffer; 308 | x: Buffer; 309 | v: Buffer; 310 | B: Buffer; 311 | A: Buffer; 312 | u: Buffer; 313 | S: Buffer; 314 | K: Buffer; 315 | M1: Buffer; 316 | }) { 317 | hexequal( 318 | inputs.I, 319 | Buffer.from("616e6472c3a9406578616d706c652e6f7267", "hex"), 320 | "I" 321 | ); 322 | hexequal( 323 | computeVerifier(params, inputs.salt, inputs.I, inputs.P), 324 | expected.v, 325 | "v" 326 | ); 327 | 328 | const client = new Client(params, inputs.salt, inputs.I, inputs.P, inputs.a); 329 | 330 | const server = new Server(params, expected.v, inputs.b); 331 | 332 | numequal(client._private.k_num, BigInteger.fromBuffer(expected.k), "k"); 333 | numequal(client._private.x_num, BigInteger.fromBuffer(expected.x), "x"); 334 | hexequal(client.computeA(), expected.A, "A"); 335 | hexequal(server.computeB(), expected.B, "B"); 336 | 337 | assert.throws(function () { 338 | client.computeM1(); 339 | }, /incomplete protocol/); 340 | assert.throws(function () { 341 | client.computeK(); 342 | }, /incomplete protocol/); 343 | assert.throws(function () { 344 | server.checkM1(expected.M1); 345 | }, /incomplete protocol/); 346 | assert.throws(function () { 347 | server.computeK(); 348 | }, /incomplete protocol/); 349 | 350 | client.setB(expected.B); 351 | numequal(client._private.u_num as BigInteger, BigInteger.fromBuffer(expected.u), "u"); 352 | hexequal(client._private.S_buf as Buffer, expected.S, "S"); 353 | hexequal(client.computeM1(), expected.M1, "M1"); 354 | hexequal(client.computeK(), expected.K, "K"); 355 | 356 | server.setA(expected.A); 357 | numequal(server._private.u_num as BigInteger, BigInteger.fromBuffer(expected.u), "u"); 358 | hexequal(server._private.S_buf as Buffer, expected.S, "S"); 359 | assert.throws(function () { 360 | server.checkM1(Buffer.from("notM1")); 361 | }, /client did not use the same password/); 362 | server.checkM1(expected.M1); 363 | hexequal(server.computeK(), expected.K, "K"); 364 | } 365 | 366 | describe("picl vectors", () => { 367 | it("vectors 1", () => { 368 | checkVectors(params, inputs_1, expected_1); 369 | }); 370 | 371 | it("vectors 2", () => { 372 | checkVectors(params, inputs_2, expected_2); 373 | }); 374 | 375 | it("vectors 3", () => { 376 | checkVectors(params, inputs_3, expected_3); 377 | }); 378 | 379 | it("vectors 4", () => { 380 | checkVectors(params, inputs_4, expected_4); 381 | }); 382 | }); 383 | -------------------------------------------------------------------------------- /src/__tests__/rfc_5054.test.ts: -------------------------------------------------------------------------------- 1 | import { Client, computeVerifier, params as _params, Server } from ".."; 2 | import { Buffer } from "node:buffer"; 3 | import { describe, it, assert } from "vitest"; 4 | import { BigInteger } from "../bigInt"; 5 | const params = _params["1024"]; 6 | 7 | /* 8 | * http://tools.ietf.org/html/rfc5054#appendix-B 9 | */ 10 | 11 | const I = Buffer.from("alice"); 12 | const P = Buffer.from("password123"); 13 | const s = Buffer.from("beb25379d1a8581eb5a727673a2441ee", "hex"); 14 | const k_expected = "7556aa045aef2cdd07abaf0f665c3e818913186f"; 15 | const x_expected = "94b7555aabe9127cc58ccf4993db6cf84d16c124"; 16 | const v_expected = ( 17 | "7e273de8 696ffc4f 4e337d05 b4b375be b0dde156 9e8fa00a 9886d812" + 18 | "9bada1f1 822223ca 1a605b53 0e379ba4 729fdc59 f105b478 7e5186f5" + 19 | "c671085a 1447b52a 48cf1970 b4fb6f84 00bbf4ce bfbb1681 52e08ab5" + 20 | "ea53d15c 1aff87b2 b9da6e04 e058ad51 cc72bfc9 033b564e 26480d78" + 21 | "e955a5e2 9e7ab245 db2be315 e2099afb" 22 | ) 23 | .split(/\s/) 24 | .join(""); 25 | const a = Buffer.from( 26 | "60975527035cf2ad1989806f0407210bc81edc04e2762a56afd529ddda2d4393", 27 | "hex" 28 | ); 29 | const b = Buffer.from( 30 | "e487cb59d31ac550471e81f00f6928e01dda08e974a004f49e61f5d105284d20", 31 | "hex" 32 | ); 33 | const A_expected = ( 34 | "61d5e490 f6f1b795 47b0704c 436f523d d0e560f0 c64115bb 72557ec4" + 35 | "4352e890 3211c046 92272d8b 2d1a5358 a2cf1b6e 0bfcf99f 921530ec" + 36 | "8e393561 79eae45e 42ba92ae aced8251 71e1e8b9 af6d9c03 e1327f44" + 37 | "be087ef0 6530e69f 66615261 eef54073 ca11cf58 58f0edfd fe15efea" + 38 | "b349ef5d 76988a36 72fac47b 0769447b" 39 | ) 40 | .split(/\s/) 41 | .join(""); 42 | const B_expected = ( 43 | "bd0c6151 2c692c0c b6d041fa 01bb152d 4916a1e7 7af46ae1 05393011" + 44 | "baf38964 dc46a067 0dd125b9 5a981652 236f99d9 b681cbf8 7837ec99" + 45 | "6c6da044 53728610 d0c6ddb5 8b318885 d7d82c7f 8deb75ce 7bd4fbaa" + 46 | "37089e6f 9c6059f3 88838e7a 00030b33 1eb76840 910440b1 b27aaeae" + 47 | "eb4012b7 d7665238 a8e3fb00 4b117b58" 48 | ) 49 | .split(/\s/) 50 | .join(""); 51 | 52 | const u_expected = "ce38b9593487da98554ed47d70a7ae5f462ef019"; 53 | const S_expected = ( 54 | "b0dc82ba bcf30674 ae450c02 87745e79 90a3381f 63b387aa f271a10d" + 55 | "233861e3 59b48220 f7c4693c 9ae12b0a 6f67809f 0876e2d0 13800d6c" + 56 | "41bb59b6 d5979b5c 00a172b4 a2a5903a 0bdcaf8a 709585eb 2afafa8f" + 57 | "3499b200 210dcc1f 10eb3394 3cd67fc8 8a2f39a4 be5bec4e c0a3212d" + 58 | "c346d7e4 74b29ede 8a469ffe ca686e5a" 59 | ) 60 | .split(/\s/) 61 | .join(""); 62 | 63 | function asHex(num: BigInteger) { 64 | return num.toBuffer().toString("hex"); 65 | } 66 | 67 | describe("RFC 5054: Test vectors", () => { 68 | it("x", () => { 69 | const client = new Client(params, s, I, P, a); 70 | assert.equal(asHex(client._private.x_num), x_expected); 71 | }); 72 | 73 | it("V", () => { 74 | let verifier = computeVerifier(params, s, I, P); 75 | assert.equal(verifier.toString("hex"), v_expected); 76 | }); 77 | 78 | it("k", () => { 79 | const client = new Client(params, s, I, P, a); 80 | assert.equal(asHex(client._private.k_num), k_expected); 81 | }); 82 | 83 | it("A", () => { 84 | const client = new Client(params, s, I, P, a); 85 | assert.equal(client.computeA().toString("hex"), A_expected); 86 | }); 87 | 88 | it("B", () => { 89 | let verifier = computeVerifier(params, s, I, P); 90 | const server = new Server(params, verifier, b); 91 | assert.equal(server.computeB().toString("hex"), B_expected); 92 | }); 93 | 94 | it("u", () => { 95 | const client = new Client(params, s, I, P, a); 96 | client.setB(Buffer.from(B_expected, "hex")); 97 | assert.equal(asHex(client._private.u_num as BigInteger), u_expected); 98 | }); 99 | 100 | it("S client", () => { 101 | const client = new Client(params, s, I, P, a); 102 | client.setB(Buffer.from(B_expected, "hex")); 103 | assert.equal(client._private.S_buf?.toString("hex"), S_expected); 104 | }); 105 | 106 | it("S server", () => { 107 | let verifier = computeVerifier(params, s, I, P); 108 | const server = new Server(params, verifier, b); 109 | server.setA(Buffer.from(A_expected, "hex")); 110 | assert.equal(server._private.S_buf?.toString("hex"), S_expected); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /src/__tests__/srp.test.ts: -------------------------------------------------------------------------------- 1 | import { Client, computeVerifier, genKey, params as _params, Server } from ".."; 2 | import { Buffer } from "node:buffer"; 3 | import { describe, beforeAll, beforeEach, it, assert } from "vitest"; 4 | 5 | const params = _params[4096]; 6 | 7 | const salt = Buffer.from("salty"); 8 | const identity = Buffer.from("alice"); 9 | const password = Buffer.from("password123"); 10 | 11 | assert(params, "missing parameters"); 12 | 13 | let client: Client; 14 | let server: Server; 15 | let a: Buffer; 16 | let A: Buffer; 17 | let b: Buffer 18 | let B: Buffer; 19 | let verifier: Buffer; 20 | let S_client: Buffer; 21 | let S_server: Buffer; 22 | 23 | describe("SRP", () => { 24 | beforeAll(() => { 25 | verifier = computeVerifier(params, salt, identity, password); 26 | assert.equal( 27 | verifier.toString("hex"), 28 | "f0e47f50f5dead8db8d93a279e3b62d6ff50854b31fbd3474a886bef916261717e84dd4fb8b4d27feaa5146db7b1cbbc274fdf96a132b5029c2cd72527427a9b9809d5a4d018252928b4fc343bc17ce63c1859d5806f5466014fc361002d8890aeb4d6316ff37331fc2761be0144c91cdd8e00ed0138c0ce51534d1b9a9ba629d7be34d2742dd4097daabc9ecb7aaad89e53c342b038f1d2adae1f2410b7884a3e9a124c357e421bccd4524467e1922660e0a4460c5f7c38c0877b65f6e32f28296282a93fc11bbabb7bb69bf1b3f9391991d8a86dd05e15000b7e38ba38a536bb0bf59c808ec25e791b8944719488b8087df8bfd7ff20822997a53f6c86f3d45d004476d6303301376bb25a9f94b552cce5ed40de5dd7da8027d754fa5f66738c7e3fc4ef3e20d625df62cbe6e7adfc21e47880d8a6ada37e60370fd4d8fc82672a90c29f2e72f35652649d68348de6f36d0e435c8bd42dd00155d35d501becc0661b43e04cdb2da84ce92b8bf49935d73d75efcbd1176d7bbccc3cc4d4b5fefcc02d478614ee1681d2ff3c711a61a7686eb852ae06fb8227be21fb8802719b1271ba1c02b13bbf0a2c2e459d9bedcc8d1269f6a785cb4563aa791b38fb038269f63f58f47e9051499549789269cc7b8ec7026fc34ba73289c4af829d5a532e723967ce9b6c023ef0fd0cfe37f51f10f19463b6534159a09ddd2f51f3b30033" 29 | ); 30 | }); 31 | 32 | beforeEach(async () => { 33 | a = await genKey(64); 34 | b = await genKey(32); 35 | }); 36 | 37 | it("use a and b", () => { 38 | client = new Client(params, salt, identity, password, a); 39 | 40 | // client produces A 41 | A = client.computeA(); 42 | 43 | // create server 44 | server = new Server(params, verifier, b); 45 | 46 | // server produces B 47 | B = server.computeB(); 48 | 49 | // server accepts A 50 | server.setA(A); 51 | 52 | // client doesn't produce M1 too early 53 | assert.throws(() => { 54 | client.computeM1(); 55 | }, /incomplete protocol/); 56 | 57 | // client accepts B 58 | client.setB(B); 59 | 60 | // client produces M1 now 61 | client.computeM1(); 62 | 63 | // server likes client's M1 64 | var serverM2 = server.checkM1(client.computeM1()); 65 | 66 | // client and server agree on K 67 | var client_K = client.computeK(); 68 | var server_K = server.computeK(); 69 | assert.equal(client_K.toString("hex"), server_K.toString("hex")); 70 | 71 | // server is authentic 72 | assert.doesNotThrow(() => { 73 | if (!serverM2) throw new Error("M2 didn't check"); 74 | client.checkM2(serverM2); 75 | }, "M2 didn't check"); 76 | }); 77 | 78 | it("constructor doesn't require 'new'", () => { 79 | client = new Client(params, salt, identity, password, a); 80 | 81 | // client produces A 82 | A = client.computeA(); 83 | 84 | // create server 85 | server = new Server(params, verifier, b); 86 | 87 | // server produces B 88 | B = server.computeB(); 89 | 90 | // server accepts A 91 | server.setA(A); 92 | 93 | // client doesn't produce M1 too early 94 | assert.throws(() => { 95 | client.computeM1(); 96 | }, /incomplete protocol/); 97 | 98 | // client accepts B 99 | client.setB(B); 100 | 101 | // client produces M1 now 102 | client.computeM1(); 103 | 104 | // server likes client's M1 105 | var serverM2 = server.checkM1(client.computeM1()); 106 | 107 | // client and server agree on K 108 | var client_K = client.computeK(); 109 | var server_K = server.computeK(); 110 | assert.equal(client_K.toString("hex"), server_K.toString("hex")); 111 | 112 | // server is authentic 113 | assert.doesNotThrow(() => { 114 | if (!serverM2) throw new Error("M2 didn't check"); 115 | client.checkM2(serverM2); 116 | }, "M2 didn't check"); 117 | }); 118 | 119 | it("server rejects wrong M1", () => { 120 | var bad_client = new Client(params, salt, identity, Buffer.from("bad"), a); 121 | var server2 = new Server(params, verifier, b); 122 | bad_client.setB(server2.computeB()); 123 | assert.throws(() => { 124 | server.checkM1(bad_client.computeM1()); 125 | }, /client did not use the same password/); 126 | }); 127 | 128 | it("server rejects bad A", () => { 129 | // client's "A" must be 1..N-1 . Reject 0 and N and N+1. We should 130 | // reject 2*N too, but our Buffer-length checks reject it before the 131 | // number itself is examined. 132 | 133 | var server2 = new Server(params, verifier, b); 134 | var Azero = Buffer.alloc(params.N_length_bits / 8); 135 | Azero.fill(0); 136 | var AN = params.N.toBuffer(); 137 | var AN1 = params.N.add(1).toBuffer(); 138 | assert.throws(() => { 139 | server2.setA(Azero); 140 | }, /invalid client-supplied 'A'/); 141 | assert.throws(() => { 142 | server2.setA(AN); 143 | }, /invalid client-supplied 'A'/); 144 | assert.throws(() => { 145 | server2.setA(AN1); 146 | }, /invalid client-supplied 'A'/); 147 | }); 148 | 149 | it("client rejects bad B", () => { 150 | // server's "B" must be 1..N-1 . Reject 0 and N and N+1 151 | var client2 = new Client(params, salt, identity, password, a); 152 | var Bzero = Buffer.alloc(params.N_length_bits / 8); 153 | Bzero.fill(0, 0, params.N_length_bits / 8); 154 | var BN = params.N.toBuffer(); 155 | var BN1 = params.N.add(1).toBuffer(); 156 | assert.throws(() => { 157 | client2.setB(Bzero); 158 | }, /invalid server-supplied 'B'/); 159 | assert.throws(() => { 160 | client2.setB(BN); 161 | }, /invalid server-supplied 'B'/); 162 | assert.throws(() => { 163 | client2.setB(BN1); 164 | }, /invalid server-supplied 'B'/); 165 | }); 166 | 167 | it("client rejects bad M2", () => { 168 | client = new Client(params, salt, identity, password, a); 169 | 170 | // client produces A 171 | A = client.computeA(); 172 | 173 | // create server 174 | server = new Server(params, verifier, b); 175 | 176 | // server produces B 177 | B = server.computeB(); 178 | 179 | // server accepts A 180 | server.setA(A); 181 | 182 | // client accepts B 183 | client.setB(B); 184 | 185 | // client produces M1 now 186 | client.computeM1(); 187 | 188 | // server likes client's M1 and we tamper with the server's M2 189 | let serverM2 = server.checkM1(client.computeM1()) + "a" as unknown as Buffer; 190 | 191 | // client and server agree on K 192 | const client_K = client.computeK(); 193 | const server_K = server.computeK(); 194 | assert.equal(client_K.toString("hex"), server_K.toString("hex")); 195 | 196 | // server is NOT authentic 197 | assert.throws(() => { 198 | client.checkM2(serverM2); 199 | }, "M2 didn't check"); 200 | }); 201 | }); 202 | -------------------------------------------------------------------------------- /src/bigInt.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from "buffer"; 2 | import { BigInteger as _BigInteger } from "jsbn"; 3 | 4 | export class BigInteger extends _BigInteger { 5 | bigNum = true; 6 | 7 | constructor(v: number | string, r?: number) { 8 | super(v.toString(), r); 9 | } 10 | 11 | toBuffer() { 12 | let h = super.toString(16); 13 | 14 | // Fix odd-length hex values from BigInteger 15 | if (h.length % 2 === 1) { 16 | h = "0" + h; 17 | } 18 | 19 | return Buffer.from(h, "hex"); 20 | } 21 | bitLength() { 22 | return super.bitLength(); 23 | } 24 | mod(n: BigInteger | _BigInteger | number): BigInteger { 25 | return this.ensureBI(super.mod(this.ensureBI(n))); 26 | } 27 | add(n: BigInteger | _BigInteger | number): BigInteger { 28 | return this.ensureBI(super.add(this.ensureBI(n))); 29 | } 30 | mul(n: BigInteger) { 31 | return this.ensureBI(super.multiply(this.ensureBI(n))); 32 | } 33 | sub(n: BigInteger) { 34 | return this.ensureBI(super.subtract(this.ensureBI(n))); 35 | } 36 | powm(n: BigInteger, m: BigInteger) { 37 | return this.ensureBI(super.modPow(this.ensureBI(n), this.ensureBI(m))); 38 | } 39 | eq(n: BigInteger) { 40 | return super.equals(this.ensureBI(n)); 41 | } 42 | ge(n: BigInteger) { 43 | return super.compareTo(n) >= 0; 44 | } 45 | le(n: BigInteger) { 46 | return super.compareTo(n) <= 0; 47 | } 48 | static fromBuffer(buffer: Buffer) { 49 | const hex = buffer.toString("hex"); 50 | return new BigInteger(hex, 16); 51 | } 52 | ensureBI(n: BigInteger | _BigInteger | number): BigInteger { 53 | if (n && typeof n === "object" && "bigNum" in n && n.bigNum) { 54 | return n as BigInteger; 55 | } 56 | 57 | return new BigInteger(n.toString()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/crypt/__tests__/crypt.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { Buffer } from 'buffer'; 3 | 4 | import {encryptAES, decryptAES, deriveKey, decryptRSAWithJWK, encryptRSAWithJWK, decryptAESToBuffer, encryptAESBuffer} from '../crypt'; 5 | 6 | describe('crypt', () => { 7 | describe('deriveKey()', () => { 8 | it('derives a key properly', async () => { 9 | const result = await deriveKey('Password', 'email', 'salt'); 10 | const expected = 'fb058595c02ae9660ed7098273bf50e49407942ecc437bf317638d76c4578eae'; 11 | expect(result).toBe(expected); 12 | }); 13 | }); 14 | 15 | describe('AES', () => { 16 | it('encrypts and decrypts', () => { 17 | const key = { 18 | kty: 'oct', 19 | alg: 'A256GCM', 20 | ext: true, 21 | key_ops: ['encrypt', 'decrypt'], 22 | k: '5hs1f2xuiNPHUp11i6SWlsqYpWe_hWPcEKucZlwBfFE', 23 | }; 24 | const resultEncrypted = encryptAES(key, 'Hello World!', 'additional data'); 25 | const resultDecrypted = decryptAES(key, resultEncrypted); 26 | expect(resultDecrypted).toEqual('Hello World!'); 27 | }); 28 | }); 29 | 30 | describe('AES Buffer', () => { 31 | it('encrypts and decrypts', () => { 32 | const key = { 33 | kty: 'oct', 34 | alg: 'A256GCM', 35 | ext: true, 36 | key_ops: ['encrypt', 'decrypt'], 37 | k: '5hs1f2xuiNPHUp11i6SWlsqYpWe_hWPcEKucZlwBfFE', 38 | }; 39 | const source = Buffer.from('Hello World!', 'utf8'); 40 | const resultEncrypted = encryptAESBuffer(key, source); 41 | const resultDecrypted = decryptAESToBuffer(key, resultEncrypted); 42 | expect(resultDecrypted.toString()).toEqual(source.toString()); 43 | }); 44 | }); 45 | 46 | describe('RSA', () => { 47 | it('encrypts and decrypts', () => { 48 | const privateKey = { 49 | alg: 'RSA-OAEP-256', 50 | kty: 'RSA', 51 | key_ops: ['decrypt'], 52 | ext: true, 53 | d: 54 | 'UkouuQID2o9Q6VyiRMmK8ETPsAHWEL2HMYwy34c4nTpM7KfqlNeMzs6HmbuEfx-bwUvTqOO4Tz7FZw4ILD6s5sE9' + 55 | 'xqIxmV-fIiqiBI4aWKozxgf9OJZWKqru3loSd923O3fI3oa9ZCTaKc1U0bYOB-XP2Q_hB2M64Hb63-McXAM0RQwN' + 56 | 'R5vh_TweqcaBiXAyhuYl2NarOwrbSlSttkhZzy4i-otulPGkW61I5rNflsSmnYEijmD7zl9EouEOYcHlJGmNLHjG' + 57 | 'nHlb-avvwvER5NZVDwd6vT61QR1wwpnYSjVH_Z_OrqJu8U2J64J_MaZPJog3KbPYqZnGcxJ9ldnSYQ', 58 | dp: 59 | 'SnO3kTAogveLWkqSuDVxQLOo4QXEq-Us_lM00dfGIuZrfWnyqOd6_NJKu-G62PCQgUMBy7r_f3N3sOseRVl_5fy3' + 60 | 'kd21n7WmnQcMGBm-VRathT0nOz-fhzbwCJwtuI1g36YtaCQEQiC4pYxMYmXaX1WGPhL4rGSNz5SMHrPvyTU', 61 | dq: 62 | 'eaK2-w2Jb7rWWYhLon-RKlzWXTPHjnt1JfLkD_D12FNE6KAcbyIETDid8_sXnqj3oCr3HrO-AuNI87zZJ4UVsy2J' + 63 | 'mvNNVFTn0s8R6TZ9_o9LCXBdBcRST5qpAax0t-duHRaKMuWPG3xAb2Uub6T42C7yd-mpOIRo7uSHFGQGzWk', 64 | e: 'AQAB', 65 | n: 66 | '5wOecCVWaDB2s-ybsCp1BskBW5fj3iH4xja5hNTfW_s-ERgbEoBA_PSF60PC1se4r_oWwFBfIRL9OUTYwYOuQOuC' + 67 | 'rgd7ODa_YBeOjzHUA1b0K5kWXaqPxkmN8kJPISyLdLjCCOwHtTFnqxL9JjnU4aIxy4OU1S5KR6v-XVeLOZtUOm4k' + 68 | 'AhLVfmdzYB1nZmq9xH1O8_acIUoWDbAWX6fIXhPhn7jCuCO4WZQVDxZ5_bc27UVhR4VYe2Our7aESUQ5ZyMtYNym' + 69 | 'o9Oy0y_m3OS6W_JR_feXBbxRCBuGf7fjnvV9ohx1ZqLpJFx9_xL7naoVCQhBDfVE31iYz3L6KTIhFQ', 70 | p: 71 | '_Pyx-puBM_BQubDg8BZsrssgmECHuieKEwlR19fckczS4dlgQVUemUTr-RqmItj7k-WMG7mWuRgbIrGO1sigpuDy' + 72 | 'uKWkg3KyqoeIvgsaJV05xu8pneVblTXJSCtNMXvYMa6mPNYudUSy8-TlLyLg_w5lUnuA8Mq8xehzCsPrM-0', 73 | q: 74 | '6cPuxuW0zaMIVAJpOcLf5D0dA-mLXxmmJoMDodamM7t7_oGYgFR3Os9gtB5TYxsUjjRCWGkDy7ru8arcbTNCnNFd' + 75 | 'PAayzTdWCW-GBs3xswggjmugGupjOnrtD-N1_fG040Ka8UqwMyI1lUapxaKhViR9TNYUz6EAOjxycf8MTMk', 76 | qi: 77 | 'FMVx12_ioueu052xgFxWdIS_lImUGTrw8Iiw_kp-KKsONtofs91A5GVRtyg_wdXpG2qyomaet1hTlHhLnoI23L2a' + 78 | 'EkQ87SokIpoR9lR8jfIRwLwKKXMc33_bRRQXvWop0yvTzmSGaC0gULcqj0OHiUR1u9Ver1ZvgGz2jh4mP_E', 79 | }; 80 | const publicKey = { 81 | alg: 'RSA-OAEP-256', 82 | kty: 'RSA', 83 | key_ops: ['encrypt'], 84 | e: 'AQAB', 85 | n: 86 | '5wOecCVWaDB2s-ybsCp1BskBW5fj3iH4xja5hNTfW_s-ERgbEoBA_PSF60PC1se4r_oWwFBfIRL9OUTYwYOuQO' + 87 | 'uCrgd7ODa_YBeOjzHUA1b0K5kWXaqPxkmN8kJPISyLdLjCCOwHtTFnqxL9JjnU4aIxy4OU1S5KR6v-XVeLOZtU' + 88 | 'Om4kAhLVfmdzYB1nZmq9xH1O8_acIUoWDbAWX6fIXhPhn7jCuCO4WZQVDxZ5_bc27UVhR4VYe2Our7aESUQ5Zy' + 89 | 'MtYNymo9Oy0y_m3OS6W_JR_feXBbxRCBuGf7fjnvV9ohx1ZqLpJFx9_xL7naoVCQhBDfVE31iYz3L6KTIhFQ', 90 | }; 91 | const resultEncrypted = encryptRSAWithJWK(publicKey, 'aaaaaaaaa'); 92 | const resultDecrypted = decryptRSAWithJWK(privateKey, resultEncrypted); 93 | const expectedDecrypted = 'aaaaaaaaa'; 94 | expect(resultDecrypted.toString()).toEqual(expectedDecrypted); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /src/crypt/base64.ts: -------------------------------------------------------------------------------- 1 | export async function decodeBase64(base64: string): Promise { 2 | let uri = 'data:application/octet-binary;base64,'; 3 | uri += base64; 4 | 5 | const res = await fetch(uri); 6 | const buffer = await res.arrayBuffer(); 7 | 8 | return new Uint8Array(buffer); 9 | } 10 | 11 | export async function encodeBase64(data: Uint8Array): Promise { 12 | const dataUri = await new Promise((resolve, reject) => { 13 | const reader = new FileReader(); 14 | reader.onload = () => { 15 | if (typeof reader.result === 'string') { 16 | resolve(reader.result); 17 | } else { 18 | reject(); 19 | } 20 | } 21 | reader.onerror = reject; 22 | reader.readAsDataURL(new Blob([data])); 23 | }); 24 | 25 | const dataAt = dataUri.indexOf(','); 26 | if (dataAt === -1) { 27 | throw new Error(`unexpected data uri output: ${dataUri}`); 28 | } 29 | 30 | return dataUri.slice(dataAt + 1); 31 | } 32 | -------------------------------------------------------------------------------- /src/crypt/box.ts: -------------------------------------------------------------------------------- 1 | import { encodeBase64, decodeBase64 } from "./base64"; 2 | import { open, seal } from "./sealedbox"; 3 | 4 | export async function encodeBox( 5 | data: T, 6 | publicKey: Uint8Array 7 | ): Promise<{ 8 | key: string; 9 | data: string; 10 | }> { 11 | const enc = new TextEncoder(); 12 | 13 | // Encode data to JSON, to bytes 14 | const rawData = enc.encode(JSON.stringify(data)); 15 | 16 | // Seal bytes into box 17 | const sealedBox = seal(rawData, publicKey); 18 | 19 | // Base64 parameters into request 20 | const body = { 21 | key: await encodeBase64(publicKey), 22 | data: await encodeBase64(sealedBox) 23 | }; 24 | 25 | return body; 26 | } 27 | 28 | export async function decodeBox( 29 | data: string, 30 | publicKey: Uint8Array, 31 | secretKey: Uint8Array 32 | ): Promise { 33 | const dec = new TextDecoder(); 34 | 35 | // Decode base64'd string 36 | const sealedBox = await decodeBase64(data); 37 | 38 | // Unseal box into bytes 39 | const rawData = open(sealedBox, publicKey, secretKey); 40 | 41 | if (!rawData) { 42 | throw new Error("Invalid data"); 43 | } 44 | 45 | // Decode and parse bytes from JSON. 46 | return JSON.parse(dec.decode(rawData)); 47 | } 48 | -------------------------------------------------------------------------------- /src/crypt/crypt.ts: -------------------------------------------------------------------------------- 1 | import { HKDF } from "./hkdf"; 2 | import { genKey } from "../srp"; 3 | import type { jsbn } from "node-forge"; 4 | import forge from "node-forge"; 5 | import { Buffer } from "buffer"; 6 | 7 | const DEFAULT_BYTE_LENGTH = 32; 8 | const DEFAULT_PBKDF2_ITERATIONS = 1e5; // 100,000 9 | 10 | /** 11 | * Generate hex signing key used for AES encryption 12 | */ 13 | export async function deriveKey(pass: string, email: string, salt: string) { 14 | const combinedSalt = await _hkdfSalt(salt, email); 15 | return _pbkdf2Passphrase(pass, combinedSalt); 16 | } 17 | 18 | /** 19 | * Encrypt with RSA256 public key 20 | */ 21 | export function encryptRSAWithJWK(publicKeyJWK: JsonWebKey, plaintext: string) { 22 | if (publicKeyJWK.alg !== "RSA-OAEP-256") { 23 | throw new Error("Public key algorithm was not RSA-OAEP-256"); 24 | } else if (publicKeyJWK.kty !== "RSA") { 25 | throw new Error("Public key type was not RSA"); 26 | } else if ( 27 | !publicKeyJWK.key_ops || 28 | !publicKeyJWK.key_ops.find((o) => o === "encrypt") 29 | ) { 30 | throw new Error('Public key does not have "encrypt" op'); 31 | } else if (!publicKeyJWK.n || !publicKeyJWK.e) { 32 | throw new Error("Public key is missing parameters"); 33 | } 34 | 35 | const encodedPlaintext = encodeURIComponent(plaintext); 36 | 37 | const n = _b64UrlToBigInt(publicKeyJWK.n); 38 | const e = _b64UrlToBigInt(publicKeyJWK.e); 39 | 40 | // @ts-expect-error node-forge typings are incomplete 41 | const publicKey = forge.rsa.setPublicKey(n, e); 42 | 43 | const encrypted = publicKey.encrypt(encodedPlaintext, "RSA-OAEP", { 44 | md: forge.md.sha256.create() 45 | }); 46 | 47 | return forge.util.bytesToHex(encrypted); 48 | } 49 | 50 | export function decryptRSAWithJWK( 51 | privateJWK: JsonWebKey, 52 | encryptedBlob: string 53 | ) { 54 | if ( 55 | !privateJWK.n || 56 | !privateJWK.e || 57 | !privateJWK.d || 58 | !privateJWK.p || 59 | !privateJWK.q || 60 | !privateJWK.dp || 61 | !privateJWK.dq || 62 | !privateJWK.qi 63 | ) { 64 | throw new Error("Private key is missing parameters"); 65 | } 66 | 67 | const n = _b64UrlToBigInt(privateJWK.n); 68 | const e = _b64UrlToBigInt(privateJWK.e); 69 | const d = _b64UrlToBigInt(privateJWK.d); 70 | const p = _b64UrlToBigInt(privateJWK.p); 71 | const q = _b64UrlToBigInt(privateJWK.q); 72 | const dP = _b64UrlToBigInt(privateJWK.dp); 73 | const dQ = _b64UrlToBigInt(privateJWK.dq); 74 | const qInv = _b64UrlToBigInt(privateJWK.qi); 75 | 76 | // @ts-expect-error node-forge typings are incomplete 77 | const privateKey = forge.rsa.setPrivateKey(n, e, d, p, q, dP, dQ, qInv); 78 | const bytes = forge.util.hexToBytes(encryptedBlob); 79 | const decrypted = privateKey.decrypt(bytes, "RSA-OAEP", { 80 | md: forge.md.sha256.create() 81 | }); 82 | 83 | return decodeURIComponent(decrypted); 84 | } 85 | 86 | export function reEncryptRSAWithJWK( 87 | privateJWK: JsonWebKey, 88 | publicJWK: JsonWebKey, 89 | encryptedBlob: string 90 | ) { 91 | const decrypted = decryptRSAWithJWK(privateJWK, encryptedBlob); 92 | return encryptRSAWithJWK(publicJWK, decrypted); 93 | } 94 | 95 | /** 96 | * Encrypt data using symmetric key 97 | * 98 | * @param jwkOrKey JWK or string representing symmetric key 99 | * @param plaintext string of data to encrypt 100 | * @param additionalData any additional public data to attach 101 | * @returns {{iv, t, d, ad}} 102 | */ 103 | export function encryptAES( 104 | jwkOrKey: string | JsonWebKey, 105 | plaintext: string, 106 | additionalData = "" 107 | ): AESMessage { 108 | // TODO: Add assertion checks for JWK 109 | const rawKey = 110 | typeof jwkOrKey === "string" ? jwkOrKey : _b64UrlToHex(jwkOrKey.k || ""); 111 | const key = forge.util.hexToBytes(rawKey); 112 | 113 | const iv = forge.random.getBytesSync(12); 114 | const cipher = forge.cipher.createCipher("AES-GCM", key); 115 | 116 | // Plaintext could contain weird unicode, so we have to encode that 117 | const encodedPlaintext = encodeURIComponent(plaintext); 118 | 119 | cipher.start({ additionalData, iv, tagLength: 128 }); 120 | cipher.update(forge.util.createBuffer(encodedPlaintext)); 121 | cipher.finish(); 122 | 123 | return { 124 | iv: forge.util.bytesToHex(iv), 125 | // @ts-expect-error node-forge typings are incomplete 126 | t: forge.util.bytesToHex(cipher.mode.tag), 127 | // @ts-expect-error node-forge typings are incomplete 128 | d: forge.util.bytesToHex(cipher.output), 129 | ad: forge.util.bytesToHex(additionalData) 130 | }; 131 | } 132 | 133 | export interface AESMessage { 134 | iv: string; 135 | t: string; 136 | d: string; 137 | ad: string; 138 | } 139 | 140 | /** 141 | * Decrypt AES using a key 142 | */ 143 | export function decryptAES(jwkOrKey: string | JsonWebKey, message: AESMessage) { 144 | // TODO: Add assertion checks for JWK 145 | const rawKey = 146 | typeof jwkOrKey === "string" ? jwkOrKey : _b64UrlToHex(jwkOrKey.k || ""); 147 | const key = forge.util.hexToBytes(rawKey); 148 | 149 | // ~~~~~~~~~~~~~~~~~~~~ // 150 | // Decrypt with AES-GCM // 151 | // ~~~~~~~~~~~~~~~~~~~~ // 152 | const decipher = forge.cipher.createDecipher("AES-GCM", key); 153 | 154 | decipher.start({ 155 | iv: forge.util.hexToBytes(message.iv), 156 | tagLength: message.t.length * 4, 157 | // @ts-expect-error node-forge typings are incomplete 158 | tag: forge.util.hexToBytes(message.t), 159 | additionalData: forge.util.hexToBytes(message.ad) 160 | }); 161 | 162 | decipher.update(forge.util.createBuffer(forge.util.hexToBytes(message.d))); 163 | 164 | if (decipher.finish()) { 165 | return decodeURIComponent(decipher.output.toString()); 166 | } else { 167 | throw new Error("Failed to decrypt data"); 168 | } 169 | } 170 | 171 | /** 172 | * Decrypt and re-encrypt with new keys 173 | */ 174 | export function reEncryptAES( 175 | oldJwkOrKey: string, 176 | newJwkOrKey: string, 177 | message: AESMessage 178 | ) { 179 | const decrypted = decryptAES(oldJwkOrKey, message); 180 | return encryptAES(newJwkOrKey, decrypted); 181 | } 182 | 183 | /** 184 | * Generate a random salt in hex 185 | */ 186 | export function getRandomHex(bytes = DEFAULT_BYTE_LENGTH) { 187 | return forge.util.bytesToHex(forge.random.getBytesSync(bytes)); 188 | } 189 | 190 | /** 191 | * Generate a random account Id 192 | */ 193 | export function generateAccountId() { 194 | return `act_${getRandomHex(DEFAULT_BYTE_LENGTH)}`; 195 | } 196 | 197 | /** 198 | * Generate a random key 199 | */ 200 | export async function srpGenKey() { 201 | const key = await genKey(); 202 | 203 | return key.toString("hex"); 204 | } 205 | 206 | /** 207 | * Generate a random AES256 key for use with symmetric encryption 208 | */ 209 | export async function generateAES256Key() { 210 | const c = window.crypto; 211 | const subtle = c ? c.subtle : null; 212 | 213 | if (subtle) { 214 | console.log("-- Using Native AES Key Generation --"); 215 | const key = await subtle.generateKey( 216 | { name: "AES-GCM", length: 256 }, 217 | true, 218 | ["encrypt", "decrypt"] 219 | ); 220 | return subtle.exportKey("jwk", key); 221 | } else { 222 | console.log("-- Using Falback Forge AES Key Generation --"); 223 | const key = forge.util.bytesToHex(forge.random.getBytesSync(32)); 224 | return { 225 | kty: "oct", 226 | alg: "A256GCM", 227 | ext: true, 228 | key_ops: ["encrypt", "decrypt"], 229 | k: _hexToB64Url(key) 230 | }; 231 | } 232 | } 233 | 234 | /** 235 | * Generate RSA keypair JWK with 2048 bits and exponent 0x10001 236 | */ 237 | export async function generateKeyPairJWK() { 238 | // NOTE: Safari has crypto.webkitSubtle, but does not support RSA-OAEP-SHA256 239 | const subtle = window.crypto && window.crypto.subtle; 240 | 241 | if (subtle) { 242 | console.log("-- Using Native RSA Generation --"); 243 | 244 | const pair = await subtle.generateKey( 245 | { 246 | name: "RSA-OAEP", 247 | publicExponent: new Uint8Array([1, 0, 1]), 248 | modulusLength: 2048, 249 | hash: "SHA-256" 250 | }, 251 | true, 252 | ["encrypt", "decrypt"] 253 | ); 254 | 255 | // TODO: Remove ! operator in favor of proper assertion 256 | if (!pair.publicKey || !pair.privateKey) { 257 | throw new Error("Unexpected error generating a keypair."); 258 | } 259 | 260 | return { 261 | publicKey: await subtle.exportKey("jwk", pair.publicKey), 262 | privateKey: await subtle.exportKey("jwk", pair.privateKey) 263 | }; 264 | } else { 265 | console.log("-- Using Forge RSA Generation --"); 266 | 267 | const pair = forge.pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 }); 268 | const privateKey = { 269 | alg: "RSA-OAEP-256", 270 | kty: "RSA", 271 | key_ops: ["decrypt"], 272 | ext: true, 273 | d: _bigIntToB64Url(pair.privateKey.d), 274 | dp: _bigIntToB64Url(pair.privateKey.dP), 275 | dq: _bigIntToB64Url(pair.privateKey.dQ), 276 | e: _bigIntToB64Url(pair.privateKey.e), 277 | n: _bigIntToB64Url(pair.privateKey.n), 278 | p: _bigIntToB64Url(pair.privateKey.p), 279 | q: _bigIntToB64Url(pair.privateKey.q), 280 | qi: _bigIntToB64Url(pair.privateKey.qInv) 281 | }; 282 | 283 | const publicKey = { 284 | alg: "RSA-OAEP-256", 285 | kty: "RSA", 286 | key_ops: ["encrypt"], 287 | e: _bigIntToB64Url(pair.publicKey.e), 288 | n: _bigIntToB64Url(pair.publicKey.n) 289 | }; 290 | 291 | return { privateKey, publicKey }; 292 | } 293 | } 294 | 295 | // ~~~~~~~~~~~~~~~~ // 296 | // Helper Functions // 297 | // ~~~~~~~~~~~~~~~~ // 298 | 299 | /** 300 | * Combine email and raw salt into usable salt 301 | */ 302 | function _hkdfSalt(rawSalt: string, rawEmail: string) { 303 | return new Promise((resolve) => { 304 | const hkdf = new HKDF("sha256", rawSalt, rawEmail); 305 | hkdf.derive("", DEFAULT_BYTE_LENGTH, (buffer: Buffer) => 306 | resolve(buffer.toString("hex")) 307 | ); 308 | }); 309 | } 310 | 311 | /** 312 | * Convert a JSBN BigInteger to a URL-safe version of base64 encoding. This 313 | * should only be used for encoding JWKs 314 | */ 315 | function _bigIntToB64Url(n: jsbn.BigInteger) { 316 | return _hexToB64Url(n.toString(16)); 317 | } 318 | 319 | function _hexToB64Url(h: string) { 320 | const bytes = forge.util.hexToBytes(h); 321 | return btoa(bytes).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_"); 322 | } 323 | 324 | function _b64UrlToBigInt(s: string) { 325 | return new forge.jsbn.BigInteger(_b64UrlToHex(s), 16); 326 | } 327 | 328 | function _b64UrlToHex(s: string) { 329 | const b64 = s.replace(/-/g, "+").replace(/_/g, "/"); 330 | return forge.util.bytesToHex(atob(b64)); 331 | } 332 | 333 | /** 334 | * Derive key from password 335 | * 336 | * @param passphrase 337 | * @param salt hex representation of salt 338 | */ 339 | async function _pbkdf2Passphrase(passphrase: string, salt: string) { 340 | if (typeof window !== 'undefined' && (!window.crypto || !window.crypto.subtle)) { 341 | return pbkdf2PassphraseForge(passphrase, salt); 342 | } 343 | 344 | try { 345 | console.log("-- Using native PBKDF2 --"); 346 | 347 | const k = await window.crypto.subtle.importKey( 348 | "raw", 349 | Buffer.from(passphrase, "utf8"), 350 | { name: "PBKDF2" }, 351 | false, 352 | ["deriveBits"] 353 | ); 354 | 355 | const algo: Pbkdf2Params = { 356 | name: "PBKDF2", 357 | salt: Buffer.from(salt, "hex"), 358 | iterations: DEFAULT_PBKDF2_ITERATIONS, 359 | hash: "SHA-256" 360 | }; 361 | 362 | const derivedKeyRaw = await window.crypto.subtle.deriveBits( 363 | algo, 364 | k, 365 | DEFAULT_BYTE_LENGTH * 8 366 | ); 367 | return Buffer.from(derivedKeyRaw).toString("hex"); 368 | } catch (e) { 369 | console.error("Unexpectedly falling back to Forge due to error:", e); 370 | return pbkdf2PassphraseForge(passphrase, salt); 371 | } 372 | } 373 | 374 | /** 375 | * Derive key from password using Forge. 376 | */ 377 | async function pbkdf2PassphraseForge(passphrase: string, salt: string) { 378 | console.log("-- Using Forge PBKDF2 --"); 379 | 380 | const derivedKeyRaw = forge.pkcs5.pbkdf2( 381 | passphrase, 382 | forge.util.hexToBytes(salt), 383 | DEFAULT_PBKDF2_ITERATIONS, 384 | DEFAULT_BYTE_LENGTH, 385 | forge.md.sha256.create() 386 | ); 387 | 388 | return forge.util.bytesToHex(derivedKeyRaw); 389 | } 390 | 391 | /** 392 | * Encrypt data using symmetric key 393 | * 394 | * @param jwkOrKey JWK or string representing symmetric key 395 | * @param buff data to encrypt 396 | * @param additionalData any additional public data to attach 397 | * @returns {{iv, t, d, ad}} 398 | */ 399 | export function encryptAESBuffer(jwkOrKey: string | JsonWebKey, buff: Buffer, additionalData = ''): AESMessage { 400 | // TODO: Add assertion checks for JWK 401 | const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k || ''); 402 | const key = forge.util.hexToBytes(rawKey); 403 | const iv = forge.random.getBytesSync(12); 404 | const cipher = forge.cipher.createCipher('AES-GCM', key); 405 | cipher.start({ 406 | additionalData, 407 | iv, 408 | tagLength: 128, 409 | }); 410 | cipher.update(forge.util.createBuffer(buff)); 411 | cipher.finish(); 412 | return { 413 | iv: forge.util.bytesToHex(iv), 414 | // @ts-expect-error -- TSCONVERSION needs to be converted to string 415 | t: forge.util.bytesToHex(cipher.mode.tag), 416 | ad: forge.util.bytesToHex(additionalData), 417 | // @ts-expect-error -- TSCONVERSION needs to be converted to string 418 | d: forge.util.bytesToHex(cipher.output), 419 | }; 420 | } 421 | 422 | /** 423 | * Decrypts AES using a key to buffer 424 | * @param jwkOrKey 425 | * @param encryptedResult 426 | * @returns {string} 427 | */ 428 | export function decryptAESToBuffer(jwkOrKey: string | JsonWebKey, encryptedResult: AESMessage) { 429 | // TODO: Add assertion checks for JWK 430 | const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k || ''); 431 | const key = forge.util.hexToBytes(rawKey); 432 | // ~~~~~~~~~~~~~~~~~~~~ // 433 | // Decrypt with AES-GCM // 434 | // ~~~~~~~~~~~~~~~~~~~~ // 435 | const decipher = forge.cipher.createDecipher('AES-GCM', key); 436 | decipher.start({ 437 | iv: forge.util.hexToBytes(encryptedResult.iv), 438 | tagLength: encryptedResult.t.length * 4, 439 | // @ts-expect-error -- TSCONVERSION needs to be converted to string 440 | tag: forge.util.hexToBytes(encryptedResult.t), 441 | additionalData: forge.util.hexToBytes(encryptedResult.ad), 442 | }); 443 | decipher.update(forge.util.createBuffer(forge.util.hexToBytes(encryptedResult.d))); 444 | 445 | if (decipher.finish()) { 446 | // @ts-expect-error -- TSCONVERSION needs to be converted to string 447 | return Buffer.from(forge.util.bytesToHex(decipher.output), 'hex'); 448 | } else { 449 | throw new Error('Failed to decrypt data'); 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /src/crypt/hkdf.ts: -------------------------------------------------------------------------------- 1 | // HKDF - https://tools.ietf.org/html/rfc5869 2 | import crypto from 'crypto'; 3 | import process from 'process'; 4 | import { Buffer } from 'buffer'; 5 | 6 | function zeros(length: number) { 7 | let buf = Buffer.alloc(length); 8 | 9 | buf.fill(0); 10 | 11 | return buf.toString(); 12 | } 13 | 14 | export class HKDF { 15 | hashAlg: string; 16 | salt: string; 17 | ikm: string; 18 | hashLength: number; 19 | prk: Buffer; 20 | 21 | constructor(hashAlg: string, salt: string, ikm: string) { 22 | this.hashAlg = hashAlg; 23 | 24 | // create the hash alg to see if it exists and get its length 25 | const hash = crypto.createHash(this.hashAlg); 26 | this.hashLength = hash.digest().length; 27 | 28 | this.salt = salt || zeros(this.hashLength); 29 | this.ikm = ikm; 30 | 31 | // now we compute the PRK 32 | const hmac = crypto.createHmac(this.hashAlg, this.salt); 33 | hmac.update(this.ikm); 34 | this.prk = hmac.digest(); 35 | }; 36 | 37 | derive(info: Buffer | crypto.BinaryLike, size: number, cb: { (buffer: Buffer): void; (arg0: any): void; }) { 38 | let prev = Buffer.alloc(0); 39 | let output: Buffer; 40 | const buffers: Buffer[] = []; 41 | const num_blocks = Math.ceil(size / this.hashLength); 42 | info = Buffer.from(info.toString()); 43 | 44 | for (let i=0; i < num_blocks; i++) { 45 | const hmac = crypto.createHmac(this.hashAlg, this.prk); 46 | hmac.update(prev); 47 | hmac.update(info); 48 | hmac.update(Buffer.from([i + 1])); 49 | prev = hmac.digest(); 50 | buffers.push(prev); 51 | } 52 | output = Buffer.concat(buffers, size); 53 | 54 | process.nextTick(function() {cb(output);}); 55 | } 56 | } -------------------------------------------------------------------------------- /src/crypt/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base64'; 2 | export * from './sealedbox'; 3 | export * from './box'; 4 | export * from './hkdf'; 5 | export * from './crypt'; -------------------------------------------------------------------------------- /src/crypt/sealedbox.ts: -------------------------------------------------------------------------------- 1 | // This is an implementation of libsodium's sealed box algorithm (also known as 2 | // "anonymous box" in Golang.) It generates an ephemeral keypair to use for the 3 | // sender, and then generates a nonce using the blake2b digest of the ephemeral 4 | // public key and the recipient public key. This is trivially implemented using 5 | // only tweetnacl's nacl.box.* namespace, and blakejs for the nonce generation. 6 | // The ephemeral public key is prepended to a sealed box, so only the recipient 7 | // keypair is needed for opening - the ephemeral private key is just discarded. 8 | import { box as naclBox } from 'tweetnacl'; 9 | import { blake2b } from 'blakejs'; 10 | 11 | export const overheadLength = naclBox.overheadLength + naclBox.publicKeyLength; 12 | 13 | export const keyPair = naclBox.keyPair; 14 | 15 | export function open( 16 | sealedbox: Uint8Array, 17 | pk: Uint8Array, 18 | sk: Uint8Array, 19 | ): Uint8Array|null { 20 | const epk = sealedbox.subarray(0, naclBox.publicKeyLength); 21 | const data = sealedbox.subarray(naclBox.publicKeyLength); 22 | return naclBox.open(data, nonce(epk, pk), epk, sk); 23 | }; 24 | 25 | export function seal(data: Uint8Array, pk: Uint8Array): Uint8Array { 26 | const sealedbox = new Uint8Array(overheadLength + data.length); 27 | const ek = naclBox.keyPair(); 28 | const box = naclBox(data, nonce(ek.publicKey, pk), pk, ek.secretKey); 29 | sealedbox.set(ek.publicKey); 30 | sealedbox.set(box, ek.publicKey.length); 31 | return sealedbox; 32 | }; 33 | 34 | function nonce(epk: Uint8Array, pk: Uint8Array): Uint8Array { 35 | const data: Uint8Array = new Uint8Array(epk.length + pk.length); 36 | data.set(epk); 37 | data.set(pk, epk.length); 38 | return blake2b(data, undefined, naclBox.nonceLength); 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as params } from "./params"; 2 | export * from "./srp"; 3 | export * from './crypt'; -------------------------------------------------------------------------------- /src/params.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SRP Group Parameters 3 | * http://tools.ietf.org/html/rfc5054#appendix-A 4 | * 5 | * The 1024-, 1536-, and 2048-bit groups are taken from software 6 | * developed by Tom Wu and Eugene Jhong for the Stanford SRP 7 | * distribution, and subsequently proven to be prime. The larger primes 8 | * are taken from [MODP], but generators have been calculated that are 9 | * primitive roots of N, unlike the generators in [MODP]. 10 | * 11 | * The 1024-bit and 1536-bit groups MUST be supported. 12 | */ 13 | 14 | // since these are meant to be used internally, all values are numbers. If 15 | // you want to add parameter sets, you'll need to convert them to bignums. 16 | 17 | import { BigInteger } from "./bigInt"; 18 | 19 | function hex(s: string) { 20 | return new BigInteger(s.split(/\s/).join(""), 16); 21 | } 22 | 23 | export type Params = { 24 | N_length_bits: number; 25 | N: BigInteger; 26 | g: BigInteger; 27 | hash: "sha1" | "sha256" | "sha512"; 28 | }; 29 | 30 | const params: Record = { 31 | 1024: { 32 | N_length_bits: 1024, 33 | N: hex( 34 | " EEAF0AB9 ADB38DD6 9C33F80A FA8FC5E8 60726187 75FF3C0B 9EA2314C" + 35 | "9C256576 D674DF74 96EA81D3 383B4813 D692C6E0 E0D5D8E2 50B98BE4" + 36 | "8E495C1D 6089DAD1 5DC7D7B4 6154D6B6 CE8EF4AD 69B15D49 82559B29" + 37 | "7BCF1885 C529F566 660E57EC 68EDBC3C 05726CC0 2FD4CBF4 976EAA9A" + 38 | "FD5138FE 8376435B 9FC61D2F C0EB06E3" 39 | ), 40 | g: hex("02"), 41 | hash: "sha1" 42 | }, 43 | 44 | 1536: { 45 | N_length_bits: 1536, 46 | N: hex( 47 | " 9DEF3CAF B939277A B1F12A86 17A47BBB DBA51DF4 99AC4C80 BEEEA961" + 48 | "4B19CC4D 5F4F5F55 6E27CBDE 51C6A94B E4607A29 1558903B A0D0F843" + 49 | "80B655BB 9A22E8DC DF028A7C EC67F0D0 8134B1C8 B9798914 9B609E0B" + 50 | "E3BAB63D 47548381 DBC5B1FC 764E3F4B 53DD9DA1 158BFD3E 2B9C8CF5" + 51 | "6EDF0195 39349627 DB2FD53D 24B7C486 65772E43 7D6C7F8C E442734A" + 52 | "F7CCB7AE 837C264A E3A9BEB8 7F8A2FE9 B8B5292E 5A021FFF 5E91479E" + 53 | "8CE7A28C 2442C6F3 15180F93 499A234D CF76E3FE D135F9BB" 54 | ), 55 | g: hex("02"), 56 | hash: "sha1" 57 | }, 58 | 59 | 2048: { 60 | N_length_bits: 2048, 61 | N: hex( 62 | " AC6BDB41 324A9A9B F166DE5E 1389582F AF72B665 1987EE07 FC319294" + 63 | "3DB56050 A37329CB B4A099ED 8193E075 7767A13D D52312AB 4B03310D" + 64 | "CD7F48A9 DA04FD50 E8083969 EDB767B0 CF609517 9A163AB3 661A05FB" + 65 | "D5FAAAE8 2918A996 2F0B93B8 55F97993 EC975EEA A80D740A DBF4FF74" + 66 | "7359D041 D5C33EA7 1D281E44 6B14773B CA97B43A 23FB8016 76BD207A" + 67 | "436C6481 F1D2B907 8717461A 5B9D32E6 88F87748 544523B5 24B0D57D" + 68 | "5EA77A27 75D2ECFA 032CFBDB F52FB378 61602790 04E57AE6 AF874E73" + 69 | "03CE5329 9CCC041C 7BC308D8 2A5698F3 A8D0C382 71AE35F8 E9DBFBB6" + 70 | "94B5C803 D89F7AE4 35DE236D 525F5475 9B65E372 FCD68EF2 0FA7111F" + 71 | "9E4AFF73" 72 | ), 73 | g: hex("02"), 74 | hash: "sha256" 75 | }, 76 | 77 | 3072: { 78 | N_length_bits: 3072, 79 | N: hex( 80 | " FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08" + 81 | "8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B" + 82 | "302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9" + 83 | "A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6" + 84 | "49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8" + 85 | "FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" + 86 | "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C" + 87 | "180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" + 88 | "3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D" + 89 | "04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D" + 90 | "B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226" + 91 | "1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" + 92 | "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC" + 93 | "E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF" 94 | ), 95 | g: hex("05"), 96 | hash: "sha256" 97 | }, 98 | 99 | 4096: { 100 | N_length_bits: 4096, 101 | N: hex( 102 | " FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08" + 103 | "8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B" + 104 | "302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9" + 105 | "A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6" + 106 | "49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8" + 107 | "FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" + 108 | "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C" + 109 | "180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" + 110 | "3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D" + 111 | "04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D" + 112 | "B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226" + 113 | "1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" + 114 | "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC" + 115 | "E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26" + 116 | "99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB" + 117 | "04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2" + 118 | "233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127" + 119 | "D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34063199" + 120 | "FFFFFFFF FFFFFFFF" 121 | ), 122 | g: hex("05"), 123 | hash: "sha256" 124 | }, 125 | 126 | 6244: { 127 | N_length_bits: 6244, 128 | N: hex( 129 | " FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08" + 130 | "8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B" + 131 | "302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9" + 132 | "A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6" + 133 | "49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8" + 134 | "FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" + 135 | "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C" + 136 | "180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" + 137 | "3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D" + 138 | "04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D" + 139 | "B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226" + 140 | "1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" + 141 | "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC" + 142 | "E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26" + 143 | "99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB" + 144 | "04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2" + 145 | "233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127" + 146 | "D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492" + 147 | "36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406" + 148 | "AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918" + 149 | "DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151" + 150 | "2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03" + 151 | "F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F" + 152 | "BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA" + 153 | "CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B" + 154 | "B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632" + 155 | "387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E" + 156 | "6DCC4024 FFFFFFFF FFFFFFFF" 157 | ), 158 | g: hex("05"), 159 | hash: "sha256" 160 | }, 161 | 162 | 8192: { 163 | N_length_bits: 8192, 164 | N: hex( 165 | " FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08" + 166 | "8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B" + 167 | "302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9" + 168 | "A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6" + 169 | "49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8" + 170 | "FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" + 171 | "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C" + 172 | "180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" + 173 | "3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D" + 174 | "04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D" + 175 | "B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226" + 176 | "1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" + 177 | "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC" + 178 | "E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26" + 179 | "99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB" + 180 | "04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2" + 181 | "233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127" + 182 | "D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492" + 183 | "36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406" + 184 | "AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918" + 185 | "DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151" + 186 | "2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03" + 187 | "F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F" + 188 | "BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA" + 189 | "CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B" + 190 | "B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632" + 191 | "387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E" + 192 | "6DBE1159 74A3926F 12FEE5E4 38777CB6 A932DF8C D8BEC4D0 73B931BA" + 193 | "3BC832B6 8D9DD300 741FA7BF 8AFC47ED 2576F693 6BA42466 3AAB639C" + 194 | "5AE4F568 3423B474 2BF1C978 238F16CB E39D652D E3FDB8BE FC848AD9" + 195 | "22222E04 A4037C07 13EB57A8 1A23F0C7 3473FC64 6CEA306B 4BCBC886" + 196 | "2F8385DD FA9D4B7F A2C087E8 79683303 ED5BDD3A 062B3CF5 B3A278A6" + 197 | "6D2A13F8 3F44F82D DF310EE0 74AB6A36 4597E899 A0255DC1 64F31CC5" + 198 | "0846851D F9AB4819 5DED7EA1 B1D510BD 7EE74D73 FAF36BC3 1ECFA268" + 199 | "359046F4 EB879F92 4009438B 481C6CD7 889A002E D5EE382B C9190DA6" + 200 | "FC026E47 9558E447 5677E9AA 9E3050E2 765694DF C81F56E8 80B96E71" + 201 | "60C980DD 98EDD3DF FFFFFFFF FFFFFFFF" 202 | ), 203 | g: hex("13"), 204 | hash: "sha256" 205 | } 206 | }; 207 | 208 | export default params; 209 | -------------------------------------------------------------------------------- /src/srp.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "crypto"; 2 | import { BigInteger } from "./bigInt"; 3 | import { Buffer } from "buffer"; 4 | 5 | const zero = new BigInteger(0); 6 | 7 | function invariant(condition: boolean, message?: string): asserts condition { 8 | if (!condition) { 9 | throw new Error(message); 10 | } 11 | } 12 | 13 | type Params = { 14 | N: BigInteger; 15 | g: BigInteger; 16 | hash: string; 17 | N_length_bits: number; 18 | }; 19 | 20 | /* 21 | * If a conversion is explicitly specified with the operator PAD(), 22 | * the integer will first be implicitly converted, then the resultant 23 | * byte-string will be left-padded with zeros (if necessary) until its 24 | * length equals the implicitly-converted length of N. 25 | */ 26 | function padTo(n: Buffer, len: number) { 27 | invariant(Buffer.isBuffer(n), "Type error: n must be a buffer"); 28 | 29 | const padding = len - n.length; 30 | invariant(padding > -1, "Negative padding. Very uncomfortable."); 31 | const result = Buffer.alloc(len); 32 | result.fill(0, 0, padding); 33 | n.copy(result, padding); 34 | invariant(result.length === len, "Padding failed"); 35 | return result; 36 | } 37 | 38 | function padToN(number: BigInteger, params: Params) { 39 | invariant(number.bigNum === true); 40 | return padTo(number.toBuffer(), params.N_length_bits / 8); 41 | } 42 | 43 | /* 44 | * compute the intermediate value x as a hash of three buffers: 45 | * salt, identity, and password. And a colon. FOUR buffers. 46 | * 47 | * x = H(s | H(I | ":" | P)) 48 | */ 49 | function getx(params: Params, salt: Buffer, I: Buffer, P: Buffer) { 50 | invariant(Buffer.isBuffer(salt), "Type error: salt (salt) must be a buffer"); 51 | invariant(Buffer.isBuffer(I), "Type error: identity (I) must be a buffer"); 52 | invariant(Buffer.isBuffer(P), "Type error: password (P) must be a buffer"); 53 | 54 | const hashIP = Buffer.from( 55 | crypto 56 | .createHash(params.hash) 57 | .update(Buffer.concat([I, Buffer.from(":"), P])) 58 | .digest() 59 | ); 60 | 61 | const hashX = Buffer.from( 62 | crypto.createHash(params.hash).update(salt).update(hashIP).digest() 63 | ); 64 | 65 | return BigInteger.fromBuffer(hashX); 66 | } 67 | 68 | /* 69 | * The verifier is calculated as described in Section 3 of [SRP-RFC]. 70 | * We give the algorithm here for convenience. 71 | * 72 | * The verifier (v) is computed based on the salt (s), user name (I), 73 | * password (P), and group parameters (N, g). 74 | * 75 | * x = H(s | H(I | ":" | P)) 76 | * v = g^x % N 77 | */ 78 | function computeVerifier(params: Params, salt: Buffer, I: Buffer, P: Buffer) { 79 | invariant(Buffer.isBuffer(salt), "Type error: salt (salt) must be a buffer"); 80 | invariant(Buffer.isBuffer(I), "Type error: identity (I) must be a buffer"); 81 | invariant(Buffer.isBuffer(P), "Type error: password (P) must be a buffer"); 82 | 83 | const v_num = params.g.powm(getx(params, salt, I, P), params.N); 84 | return padToN(v_num, params); 85 | } 86 | 87 | /* 88 | * Calculate the SRP-6 multiplier 89 | */ 90 | function getk(params: Params) { 91 | const k_buf = crypto 92 | .createHash(params.hash) 93 | .update(padToN(params.N, params)) 94 | .update(padToN(params.g, params)) 95 | .digest(); 96 | return BigInteger.fromBuffer(k_buf); 97 | } 98 | 99 | /* 100 | * Generate a random key 101 | */ 102 | async function genKey(bytes = 32) { 103 | return new Promise((resolve, reject) => { 104 | crypto.randomBytes(bytes, function (err, buf) { 105 | if (err) { 106 | reject(err); 107 | } 108 | 109 | resolve(Buffer.from(buf)); 110 | }); 111 | }); 112 | } 113 | 114 | /* 115 | * The server key exchange message also contains the server's public 116 | * value (B). The server calculates this value as B = k*v + g^b % N, 117 | * where b is a random number that SHOULD be at least 256 bits in length 118 | * and k = H(N | PAD(g)). 119 | * 120 | * Note: as the tests imply, the entire expression is mod N. 121 | */ 122 | function getB(params: Params, k: BigInteger, v: BigInteger, b: BigInteger) { 123 | invariant(v.bigNum === true); 124 | invariant(k.bigNum === true); 125 | invariant(b.bigNum === true); 126 | 127 | var N = params.N; 128 | var r = k.mul(v).add(params.g.powm(b, N)).mod(N); 129 | return padToN(r, params); 130 | } 131 | 132 | /* 133 | * The client key exchange message carries the client's public value 134 | * (A). The client calculates this value as A = g^a % N, where a is a 135 | * random number that SHOULD be at least 256 bits in length. 136 | * 137 | * Note: for this implementation, we take that to mean 256/8 bytes. 138 | */ 139 | function getA(params: Params, a_num: BigInteger) { 140 | invariant(a_num.bigNum === true); 141 | 142 | if (Math.ceil(a_num.bitLength() / 8) < 256 / 8) { 143 | console.warn( 144 | "getA: client key length", 145 | a_num.bitLength(), 146 | "is less than the recommended 256" 147 | ); 148 | } 149 | return padToN(params.g.powm(a_num, params.N), params); 150 | } 151 | 152 | /* 153 | * getu() hashes the two public messages together, to obtain a scrambling 154 | * parameter "u" which cannot be predicted by either party ahead of time. 155 | * This makes it safe to use the message ordering defined in the SRP-6a 156 | * paper, in which the server reveals their "B" value before the client 157 | * commits to their "A" value. 158 | */ 159 | function getu(params: Params, A: Buffer, B: Buffer) { 160 | invariant(Buffer.isBuffer(A), "Type error: A must be a buffer"); 161 | invariant( 162 | A.length === params.N_length_bits / 8, 163 | "A was " + A.length + ", expected " + params.N_length_bits / 8 164 | ); 165 | invariant(Buffer.isBuffer(B), "Type error: B must be a buffer"); 166 | invariant( 167 | B.length === params.N_length_bits / 8, 168 | "B was " + B.length + ", expected " + params.N_length_bits / 8 169 | ); 170 | 171 | const u_buf = crypto.createHash(params.hash).update(A).update(B).digest(); 172 | return BigInteger.fromBuffer(u_buf); 173 | } 174 | 175 | /* 176 | * The TLS premaster secret as calculated by the client 177 | */ 178 | 179 | function client_getS( 180 | params: Params, 181 | k_num: BigInteger, 182 | x_num: BigInteger, 183 | a_num: BigInteger, 184 | B_num: BigInteger, 185 | u_num: BigInteger 186 | ) { 187 | invariant(k_num.bigNum === true); 188 | invariant(x_num.bigNum === true); 189 | invariant(a_num.bigNum === true); 190 | invariant(B_num.bigNum === true); 191 | invariant(u_num.bigNum === true); 192 | 193 | const { g, N } = params; 194 | 195 | if (zero.ge(B_num) || N.le(B_num)) { 196 | throw new Error("invalid server-supplied 'B', must be 1..N-1"); 197 | } 198 | const S_num = B_num.sub(k_num.mul(g.powm(x_num, N))) 199 | .powm(a_num.add(u_num.mul(x_num)), N) 200 | .mod(N); 201 | return padToN(S_num, params); 202 | } 203 | 204 | /* 205 | * The TLS premastersecret as calculated by the server 206 | */ 207 | function server_getS( 208 | params: Params, 209 | v_num: BigInteger, 210 | A_num: BigInteger, 211 | b_num: BigInteger, 212 | u_num: BigInteger 213 | ) { 214 | invariant(v_num.bigNum === true); 215 | invariant(A_num.bigNum === true); 216 | invariant(b_num.bigNum === true); 217 | invariant(u_num.bigNum === true); 218 | 219 | const { N } = params; 220 | 221 | if (zero.ge(A_num) || N.le(A_num)) 222 | throw new Error("invalid client-supplied 'A', must be 1..N-1"); 223 | const S_num = A_num.mul(v_num.powm(u_num, N)).powm(b_num, N).mod(N); 224 | return padToN(S_num, params); 225 | } 226 | 227 | /* 228 | * Compute the shared session key K from S 229 | */ 230 | function getK(params: Params, S_buf: Buffer) { 231 | invariant(Buffer.isBuffer(S_buf), "Type error: S must be a buffer"); 232 | invariant( 233 | S_buf.length === params.N_length_bits / 8, 234 | "S was " + S_buf.length + ", expected " + params.N_length_bits / 8 235 | ); 236 | 237 | return Buffer.from(crypto.createHash(params.hash).update(S_buf).digest()); 238 | } 239 | 240 | function getM1(params: Params, A_buf: Buffer, B_buf: Buffer, S_buf: Buffer) { 241 | invariant(Buffer.isBuffer(A_buf), "Type error: A must be a buffer"); 242 | invariant( 243 | A_buf.length === params.N_length_bits / 8, 244 | "A was " + A_buf.length + ", expected " + params.N_length_bits / 8 245 | ); 246 | 247 | invariant(Buffer.isBuffer(B_buf), "Type error: B must be a buffer"); 248 | invariant( 249 | B_buf.length === params.N_length_bits / 8, 250 | "B was " + B_buf.length + ", expected " + params.N_length_bits / 8 251 | ); 252 | 253 | invariant(Buffer.isBuffer(S_buf), "Type error: S must be a buffer"); 254 | invariant( 255 | S_buf.length === params.N_length_bits / 8, 256 | "S was " + S_buf.length + ", expected " + params.N_length_bits / 8 257 | ); 258 | 259 | return Buffer.from( 260 | crypto 261 | .createHash(params.hash) 262 | .update(A_buf) 263 | .update(B_buf) 264 | .update(S_buf) 265 | .digest() 266 | ); 267 | } 268 | 269 | function getM2(params: Params, A_buf: Buffer, M_buf: Buffer, K_buf: Buffer) { 270 | invariant(Buffer.isBuffer(A_buf), "Type error: A must be a buffer"); 271 | invariant( 272 | A_buf.length === params.N_length_bits / 8, 273 | "A was " + A_buf.length + ", expected " + params.N_length_bits / 8 274 | ); 275 | invariant(Buffer.isBuffer(M_buf), "Type error: M must be a buffer"); 276 | invariant(Buffer.isBuffer(K_buf), "Type error: K must be a buffer"); 277 | 278 | return Buffer.from( 279 | crypto 280 | .createHash(params.hash) 281 | .update(A_buf) 282 | .update(M_buf) 283 | .update(K_buf) 284 | .digest() 285 | ); 286 | } 287 | 288 | function equal(buf1: Buffer, buf2: Buffer) { 289 | // Constant-time comparison. A drop in the ocean compared to our 290 | // non-constant-time modexp operations, but still good practice. 291 | var mismatch = buf1.length - buf2.length; 292 | if (mismatch) { 293 | return false; 294 | } 295 | for (var i = 0; i < buf1.length; i++) { 296 | mismatch |= buf1[i] ^ buf2[i]; 297 | } 298 | return mismatch === 0; 299 | } 300 | 301 | export class Client { 302 | _private: { 303 | params: Params; 304 | k_num: BigInteger; 305 | x_num: BigInteger; 306 | a_num: BigInteger; 307 | u_num?: BigInteger; 308 | A_buf: Buffer; 309 | K_buf?: Buffer; 310 | M1_buf?: Buffer; 311 | M2_buf?: Buffer; 312 | S_buf?: Buffer; 313 | }; 314 | 315 | constructor( 316 | params: Params, 317 | salt_buf: Buffer, 318 | identity_buf: Buffer, 319 | password_buf: Buffer, 320 | secret1_buf: Buffer 321 | ) { 322 | invariant( 323 | Buffer.isBuffer(salt_buf), 324 | "Type error: salt (salt) must be a buffer" 325 | ); 326 | invariant( 327 | Buffer.isBuffer(identity_buf), 328 | "Type error: identity (I) must be a buffer" 329 | ); 330 | invariant( 331 | Buffer.isBuffer(password_buf), 332 | "Type error: password (P) must be a buffer" 333 | ); 334 | invariant( 335 | Buffer.isBuffer(secret1_buf), 336 | "Type error: secret1 must be a buffer" 337 | ); 338 | 339 | const a_num = BigInteger.fromBuffer(secret1_buf); 340 | 341 | this._private = { 342 | params: params, 343 | k_num: getk(params), 344 | x_num: getx(params, salt_buf, identity_buf, password_buf), 345 | a_num, 346 | A_buf: getA(params, a_num) 347 | }; 348 | } 349 | 350 | computeA() { 351 | return this._private.A_buf; 352 | } 353 | 354 | setB(B_buf: Buffer) { 355 | var p = this._private; 356 | var B_num = BigInteger.fromBuffer(B_buf); 357 | var u_num = getu(p.params, p.A_buf, B_buf); 358 | var S_buf = client_getS(p.params, p.k_num, p.x_num, p.a_num, B_num, u_num); 359 | p.K_buf = getK(p.params, S_buf); 360 | p.M1_buf = getM1(p.params, p.A_buf, B_buf, S_buf); 361 | p.M2_buf = getM2(p.params, p.A_buf, p.M1_buf, p.K_buf); 362 | p.u_num = u_num; // only for tests 363 | p.S_buf = S_buf; // only for tests 364 | } 365 | 366 | computeM1() { 367 | invariant( 368 | typeof this._private.M1_buf !== "undefined", 369 | "incomplete protocol" 370 | ); 371 | 372 | return this._private.M1_buf; 373 | } 374 | 375 | checkM2(serverM2_buf: Buffer) { 376 | invariant( 377 | typeof this._private.M2_buf !== "undefined" && 378 | equal(this._private.M2_buf, serverM2_buf), 379 | "M2 didn't check" 380 | ); 381 | } 382 | 383 | computeK() { 384 | invariant( 385 | typeof this._private.K_buf !== "undefined", 386 | "incomplete protocol" 387 | ); 388 | 389 | return this._private.K_buf; 390 | } 391 | } 392 | 393 | export class Server { 394 | _private: { 395 | params: Params; 396 | k_num: BigInteger; 397 | b_num: BigInteger; 398 | v_num: BigInteger; 399 | B_buf: Buffer; 400 | u_num?: BigInteger; 401 | A_buf?: Buffer; 402 | K_buf?: Buffer; 403 | M1_buf?: Buffer; 404 | M2_buf?: Buffer; 405 | S_buf?: Buffer; 406 | }; 407 | 408 | constructor(params: Params, verifier_buf: Buffer, secret2_buf: Buffer) { 409 | invariant( 410 | Buffer.isBuffer(verifier_buf), 411 | "Type error: verifier must be a buffer" 412 | ); 413 | invariant( 414 | Buffer.isBuffer(secret2_buf), 415 | "Type error: secret2 must be a buffer" 416 | ); 417 | 418 | const k_num = getk(params); 419 | const b_num = BigInteger.fromBuffer(secret2_buf); 420 | const v_num = BigInteger.fromBuffer(verifier_buf); 421 | 422 | this._private = { 423 | params: params, 424 | k_num, 425 | b_num, 426 | v_num, 427 | B_buf: getB(params, k_num, v_num, b_num) 428 | }; 429 | } 430 | 431 | computeB() { 432 | return this._private.B_buf; 433 | } 434 | 435 | setA(A_buf: Buffer) { 436 | const p = this._private; 437 | const A_num = BigInteger.fromBuffer(A_buf); 438 | const u_num = getu(p.params, A_buf, p.B_buf); 439 | const S_buf = server_getS(p.params, p.v_num, A_num, p.b_num, u_num); 440 | p.K_buf = getK(p.params, S_buf); 441 | p.M1_buf = getM1(p.params, A_buf, p.B_buf, S_buf); 442 | p.M2_buf = getM2(p.params, A_buf, p.M1_buf, p.K_buf); 443 | p.u_num = u_num; // only for tests 444 | p.S_buf = S_buf; // only for tests 445 | } 446 | 447 | checkM1(clientM1_buf: Buffer) { 448 | invariant( 449 | typeof this._private.M1_buf !== "undefined", 450 | "incomplete protocol" 451 | ); 452 | invariant( 453 | equal(this._private.M1_buf, clientM1_buf), 454 | "client did not use the same password" 455 | ); 456 | 457 | return this._private.M2_buf; 458 | } 459 | 460 | computeK() { 461 | invariant( 462 | typeof this._private.K_buf !== "undefined", 463 | "incomplete protocol" 464 | ); 465 | 466 | return this._private.K_buf; 467 | } 468 | } 469 | 470 | export { genKey, computeVerifier }; 471 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true, 6 | "emitDeclarationOnly": true, 7 | "declaration": true, 8 | "isolatedModules": true, 9 | "lib": ["es2015", "dom"], 10 | "skipLibCheck": true, 11 | "outDir": "dist", 12 | "module": "ESNext", 13 | "moduleResolution": "node" 14 | }, 15 | "include": ["src"], 16 | "exclude": ["**/node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "path"; 2 | import { nodePolyfills } from "vite-plugin-node-polyfills"; 3 | import { defineConfig } from "vite"; 4 | 5 | export default defineConfig({ 6 | build: { 7 | lib: { 8 | entry: resolve(__dirname, "src/index.ts"), 9 | name: "insomnia-srp-client", 10 | // the proper extensions will be added 11 | fileName: "index" 12 | }, 13 | rollupOptions: { 14 | // make sure to externalize deps that shouldn't be bundled 15 | // into your library 16 | output: { 17 | // Provide global variables to use in the UMD build 18 | // for externalized deps 19 | } 20 | } 21 | }, 22 | plugins: [nodePolyfills()] 23 | }); 24 | --------------------------------------------------------------------------------