├── .github ├── get-pkg-version.js ├── set-pkg-replacer-name.js └── workflows │ └── main.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── benchmarks └── test.js ├── lib └── index.js ├── native ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── build.rs └── src │ ├── generate.rs │ ├── hash.rs │ ├── lib.rs │ ├── load_create.rs │ ├── misc.rs │ ├── secret.rs │ ├── sig.rs │ ├── unbox.rs │ └── utils.rs ├── package.json └── test-extra └── index.js /.github/get-pkg-version.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var pkg = require('../package.json'); 4 | console.log(pkg.version.replace(/-.*/g, '')); 5 | -------------------------------------------------------------------------------- /.github/set-pkg-replacer-name.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var pkg = require('../package.json'); 5 | pkg.name = 'ssb-keys'; 6 | var newPkgString = JSON.stringify(pkg, null, 2) + '\n'; 7 | fs.writeFileSync(__dirname + '/../package.json', newPkgString); 8 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | test: 14 | name: Test 15 | 16 | strategy: 17 | matrix: 18 | node-version: [12.x] 19 | os: [ubuntu-latest, macos-latest, windows-latest] 20 | 21 | runs-on: ${{ matrix.os }} 22 | 23 | steps: 24 | - name: Checkout the repo 25 | uses: actions/checkout@v2 26 | - name: Set up Rust 27 | uses: actions-rs/toolchain@v1 28 | with: 29 | toolchain: stable 30 | - name: Set up Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v2 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | - name: Set up pnpm 5 35 | uses: pnpm/action-setup@v1.2.1 36 | with: 37 | version: 5.7.x 38 | 39 | - name: pnpm install 40 | run: pnpm install 41 | 42 | - name: Compile .node 43 | run: npm run build-debug 44 | 45 | - name: Test 46 | run: npm test 47 | 48 | build-release: 49 | name: Build release for Node 50 | needs: test 51 | if: ${{ startsWith(github.event.head_commit.message, 'release') }} 52 | 53 | strategy: 54 | matrix: 55 | node-version: [10.x, 12.x, 14.x] 56 | os: [ubuntu-latest, macos-latest, windows-latest] 57 | 58 | runs-on: ${{ matrix.os }} 59 | 60 | steps: 61 | - name: Checkout the repo 62 | uses: actions/checkout@v2 63 | - name: Set up Rust 64 | uses: actions-rs/toolchain@v1 65 | with: 66 | toolchain: stable 67 | - name: Set up Node.js ${{ matrix.node-version }} 68 | uses: actions/setup-node@v2 69 | with: 70 | node-version: ${{ matrix.node-version }} 71 | - name: Set up pnpm 5 72 | uses: pnpm/action-setup@v1.2.1 73 | with: 74 | version: 5.7.x 75 | 76 | - name: pnpm install 77 | run: pnpm install 78 | 79 | - name: Compile .node 80 | run: npm run build-release 81 | 82 | - name: Tag .node as prebuild 83 | run: npm run tag-prebuild 84 | 85 | - name: Upload prebuild artifacts 86 | uses: actions/upload-artifact@v2 87 | with: 88 | name: prebuilds 89 | path: prebuilds/ 90 | 91 | build-release-electron: 92 | name: Build release for Electron 93 | needs: test 94 | if: ${{ startsWith(github.event.head_commit.message, 'release') }} 95 | 96 | strategy: 97 | matrix: 98 | electron-version: [7, 8, 9, 10] 99 | node-version: [12.x] 100 | os: [ubuntu-latest, macos-latest, windows-latest] 101 | 102 | runs-on: ${{ matrix.os }} 103 | 104 | steps: 105 | - name: Checkout the repo 106 | uses: actions/checkout@v2 107 | - name: Set up Rust 108 | uses: actions-rs/toolchain@v1 109 | with: 110 | toolchain: stable 111 | - name: Set up Node.js ${{ matrix.node-version }} 112 | uses: actions/setup-node@v2 113 | with: 114 | node-version: ${{ matrix.node-version }} 115 | - name: Set up pnpm 5 116 | uses: pnpm/action-setup@v1.2.1 117 | with: 118 | version: 5.7.x 119 | 120 | - name: pnpm install 121 | run: pnpm install 122 | 123 | - name: Install Electron ${{ matrix.electron-version }} 124 | run: pnpm install electron@${{ matrix.electron-version }} 125 | 126 | - name: Compile .node 127 | run: npm run build-release-electron 128 | 129 | - name: Tag .node as prebuild 130 | run: npm run tag-prebuild-electron 131 | 132 | - name: Upload prebuild artifacts 133 | uses: actions/upload-artifact@v2 134 | with: 135 | name: prebuilds 136 | path: prebuilds/ 137 | 138 | publish: 139 | name: NPM Publish 140 | needs: [build-release, build-release-electron] 141 | if: ${{ startsWith(github.event.head_commit.message, 'release') }} 142 | 143 | strategy: 144 | matrix: 145 | node-version: [12.x] 146 | os: [ubuntu-latest] 147 | 148 | runs-on: ${{ matrix.os }} 149 | 150 | steps: 151 | - name: Checkout the repo 152 | uses: actions/checkout@v2 153 | - name: Set up Node.js ${{ matrix.node-version }} 154 | uses: actions/setup-node@v2 155 | with: 156 | node-version: ${{ matrix.node-version }} 157 | registry-url: https://registry.npmjs.org/ 158 | 159 | - name: Download prebuild artifacts 160 | uses: actions/download-artifact@v2 161 | with: 162 | name: prebuilds 163 | path: prebuilds 164 | 165 | - name: Check prebuilds 166 | run: ls -laR prebuilds 167 | 168 | - name: NPM Publish 169 | run: npm publish --access public 170 | env: 171 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 172 | 173 | replace: 174 | name: Update replacer branch 175 | needs: [publish] 176 | if: ${{ startsWith(github.event.head_commit.message, 'release') }} 177 | 178 | strategy: 179 | matrix: 180 | node-version: [12.x] 181 | os: [ubuntu-latest] 182 | 183 | runs-on: ${{ matrix.os }} 184 | 185 | steps: 186 | - name: Checkout the repo 187 | uses: actions/checkout@v2 188 | - name: Set up Node.js ${{ matrix.node-version }} 189 | uses: actions/setup-node@v2 190 | with: 191 | node-version: ${{ matrix.node-version }} 192 | 193 | - name: Download prebuild artifacts 194 | uses: actions/download-artifact@v2 195 | with: 196 | name: prebuilds 197 | path: prebuilds 198 | 199 | - name: Check prebuilds 200 | run: ls -laR prebuilds 201 | 202 | - name: Set package.json name to "ssb-keys" 203 | run: node .github/set-pkg-replacer-name.js 204 | 205 | - name: Detect version to replace 206 | run: echo "BRANCH_VERSION=$(node .github/get-pkg-version.js)" >> $GITHUB_ENV 207 | 208 | - name: Delete git branch replace-${{ env.BRANCH_VERSION }} 209 | uses: dawidd6/action-delete-branch@v3 210 | continue-on-error: true 211 | with: 212 | github_token: ${{ secrets.GITHUB_TOKEN }} 213 | branches: replace-${{ env.BRANCH_VERSION }} 214 | 215 | - name: Push git branch replace-${{ env.BRANCH_VERSION }} 216 | uses: github-actions-x/commit@v2.7 217 | with: 218 | github-token: ${{ secrets.GITHUB_TOKEN }} 219 | push-branch: 'replace-${{ env.BRANCH_VERSION }}' 220 | commit-message: 'update replacer branch' 221 | force-add: 'true' 222 | files: package.json prebuilds/ 223 | name: Andre Staltz 224 | email: andre@staltz.com 225 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | native/target 2 | native/index.node 3 | native/artifacts.json 4 | **/*~ 5 | **/node_modules 6 | **/.DS_Store 7 | .vscode 8 | pnpm-lock.yaml 9 | /test-compat -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | native/target 3 | native/index.node 4 | native/artifacts.json 5 | **/*~ 6 | **/node_modules 7 | **/.DS_Store 8 | .vscode 9 | .gitignore 10 | pnpm-lock.yaml 11 | /test-compat 12 | /test-extra -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssb-keys-neon 2 | 3 | > A drop-in replacement of `ssb-keys`, implemented in Rust and delivered as a native module in Node.js 4 | 5 | ``` 6 | npm install ssb-keys-neon 7 | ``` 8 | 9 | ## Motivation 10 | 11 | It would make a lot of sense for SSB to be implemented in Rust, a language for safety and performance. [Sunrise Choir](https://github.com/sunrise-choir/) has written a lot of SSB modules in Rust, but these are not yet running in production, simply because it requires a lot of work to migrate from Node.js (and/or Electron) to Rust. 12 | 13 | [Neon](https://neon-bindings.com) allows you to create native modules for Node.js written in Rust. This is great, because it allows us to use Sunrise Choir's modules directly inside Node.js! 14 | 15 | ssb-keys-neon does exactly that, it runs [ssb-crypto](https://github.com/sunrise-choir/ssb-crypto), [ssb-keyfile](https://github.com/sunrise-choir/ssb-keyfile), [ssb-multiformats](https://github.com/sunrise-choir/ssb-multiformats) (Rust crates) under the hood, but provides an API that **perfectly mirrors** that of `ssb-keys`. There is no code you need to migrate, just replace `ssb-keys` with `ssb-keys-neon` and it's done. 16 | 17 | We even run the same test suite for `ssb-keys` on `ssb-keys-neon`. 18 | 19 | ## Usage 20 | 21 | There are two ways you can use this npm package. 22 | 23 | ### Option 1: (easiest) automatically replace 24 | 25 | In your **package.json**, assuming you already have `ssb-keys`, you can replace its *implementation* by pointing to the GitHub repo for `ssb-keys-neon`: 26 | 27 | ```diff 28 | // ... 29 | "dependencies": { 30 | "ssb-ebt": "^5.6.7", 31 | "ssb-friends": "^4.1.4", 32 | "ssb-invite": "^2.1.3", 33 | - "ssb-keys": "7.2.0", 34 | + "ssb-keys": "staltz/ssb-keys-neon#replace-7.2.0", 35 | "ssb-lan": "^0.2.0", 36 | "ssb-logging": "^1.0.0", 37 | "ssb-markdown": "^6.0.4", 38 | } 39 | // ... 40 | ``` 41 | 42 | This is the easiest method because you only need to change `package.json`, your code can still `require('ssb-keys')` and under the hood it will load `ssb-keys-neon`. 43 | 44 | Note that you **cannot specify version ranges**. If you previously had `"ssb-keys": "7.x.x"`, you will have to specify an exact version when you write `"staltz/ssb-keys-neon#replace-7.2.0"`, you cannot write `"staltz/ssb-keys-neon#replace-7.x.x"`. 45 | 46 | ### Option 2: manually replace 47 | 48 | This method gives you more control over the usage of `ssb-keys-neon`, as well as allows you to specify version ranges. Just *remove ssb-keys* from your package.json, and *add ssb-keys-neon*: 49 | 50 | ```diff 51 | // ... 52 | "dependencies": { 53 | "ssb-ebt": "^5.6.7", 54 | "ssb-friends": "^4.1.4", 55 | "ssb-invite": "^2.1.3", 56 | - "ssb-keys": "8.0.0", 57 | + "ssb-keys-neon": ">=8.0.0-1", 58 | "ssb-lan": "^0.2.0", 59 | "ssb-logging": "^1.0.0", 60 | "ssb-markdown": "^6.0.4", 61 | } 62 | // ... 63 | ``` 64 | 65 | **Then**, you also have to replace usages of ssb-keys manually in JavaScript source files: 66 | 67 | ```diff 68 | -var ssbKeys = require('ssb-keys') 69 | +var ssbKeys = require('ssb-keys-neon') 70 | 71 | // ... 72 | ``` 73 | 74 | ## Versioning and support 75 | 76 | `ssb-keys-neon@X.Y.Z-num` is compatible with `ssb-keys@X.Y.Z`. Versions older than 8.0.0 still use the old name `ssb-neon-keys` so be sure to type it correctly. 77 | 78 | Versions of ssb-keys that are mirrored by ssb-keys-neon currently include (and which platforms are guaranteed to be supported): 79 | 80 |
81 | 8.0.0 (click here to see which platforms are supported) 82 | 83 | As of `ssb-keys-neon@8.0.0-5` 84 | 85 | - macOS (darwin-x64) 86 | - Node 10.x 87 | - Node 12.x 88 | - Node 14.x 89 | - Electron 7.x 90 | - Electron 8.x 91 | - Electron 9.x 92 | - Electron 10.x 93 | - Linux (linux-x64) 94 | - Node 10.x 95 | - Node 12.x 96 | - Node 14.x 97 | - Electron 7.x 98 | - Electron 8.x 99 | - Electron 9.x 100 | - Electron 10.x 101 | - Windows (win32-x64) 102 | - Node 10.x 103 | - Node 12.x 104 | - Node 14.x 105 | - Electron 7.x 106 | - Electron 8.x 107 | - Electron 9.x 108 | - Electron 10.x 109 | 110 |
111 | 112 |
113 | 7.2.2 (click here to see which platforms are supported) 114 | 115 | **As of `ssb-neon-keys@7.2.2-2`** 116 | 117 | - macOS (darwin-x64) 118 | - Node 10.x 119 | - Node 12.x 120 | - Node 14.x 121 | - Electron 7.x 122 | - Electron 8.x 123 | - Electron 9.x 124 | - Electron 10.x 125 | - Linux (linux-x64) 126 | - Node 10.x 127 | - Node 12.x 128 | - Node 14.x 129 | - Electron 7.x 130 | - Electron 8.x 131 | - Electron 9.x 132 | - Electron 10.x 133 | - Windows (win32-x64) 134 | - Node 10.x 135 | - Node 12.x 136 | - Node 14.x 137 | - Electron 7.x 138 | - Electron 8.x 139 | - Electron 9.x 140 | - Electron 10.x 141 | 142 |
143 | 144 |
145 | 7.2.1 (click here to see which platforms are supported) 146 | 147 | **As of `ssb-neon-keys@7.2.1-3`** 148 | 149 | - macOS (darwin-x64) 150 | - Node 10.x 151 | - Node 12.x 152 | - Node 14.x 153 | - Electron 7.x 154 | - Electron 8.x 155 | - Electron 9.x 156 | - Electron 10.x 157 | - Linux (linux-x64) 158 | - Node 10.x 159 | - Node 12.x 160 | - Node 14.x 161 | - Electron 7.x 162 | - Electron 8.x 163 | - Electron 9.x 164 | - Electron 10.x 165 | - Windows (win32-x64) 166 | - Node 10.x 167 | - Node 12.x 168 | - Node 14.x 169 | - Electron 7.x 170 | - Electron 8.x 171 | - Electron 9.x 172 | - Electron 10.x 173 | 174 |
175 | 176 |
177 | 7.2.0 (click here to see which platforms are supported) 178 | 179 | **As of `ssb-neon-keys@7.2.0-18`** 180 | 181 | - macOS (darwin-x64) 182 | - Node 10.x 183 | - Node 12.x 184 | - Node 14.x 185 | - Electron 7.x 186 | - Electron 8.x 187 | - Electron 9.x 188 | - Electron 10.x 189 | - Linux (linux-x64) 190 | - Node 10.x 191 | - Node 12.x 192 | - Node 14.x 193 | - Electron 7.x 194 | - Electron 8.x 195 | - Electron 9.x 196 | - Electron 10.x 197 | - Windows (win32-x64) 198 | - Node 10.x 199 | - Node 12.x 200 | - Node 14.x 201 | - Electron 7.x 202 | - Electron 8.x 203 | - Electron 9.x 204 | - Electron 10.x 205 | 206 |
207 | 208 | ## License 209 | 210 | LGPL-3.0 211 | -------------------------------------------------------------------------------- /benchmarks/test.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const path = require('path'); 3 | const crypto = require('crypto'); 4 | const ssbKeys = require('ssb-keys'); 5 | const ssbKeysNeon = require('../'); 6 | 7 | function test(ssbKeys) { 8 | const filename = path.join( 9 | os.tmpdir(), 10 | 'ssbkeys' + Math.floor(Math.random() * 10e4), 11 | ); 12 | const rainbow = 'somewhere-over-the-rainbow-way-up-high'; 13 | const ed25519Str = 'ed25519'; 14 | const buf = Buffer.from(rainbow); 15 | const hmackey = crypto.randomBytes(32); 16 | const ptxt = {okay: true}; 17 | 18 | const before = Date.now(); 19 | const alice = ssbKeys.loadOrCreateSync(filename); 20 | const alicePrivate = alice.private; 21 | const bob = ssbKeys.generate(ed25519Str); 22 | const bobId = bob.id; 23 | const bobPrivate = bob.private; 24 | const recps = [alice.public, bob.public]; 25 | ssbKeys.getTag(bobId); 26 | ssbKeys.verifyObj(alice, ssbKeys.signObj(alicePrivate, hmackey, ptxt)); 27 | ssbKeys.hash(rainbow); 28 | for (let i = 0; i < 10e3; i++) { 29 | ssbKeys.unbox(ssbKeys.box(ptxt, recps), bobPrivate); 30 | ssbKeys.secretUnbox(ssbKeys.secretBox(ptxt, buf), buf); 31 | } 32 | const after = Date.now(); 33 | 34 | return after - before; 35 | } 36 | 37 | test(ssbKeysNeon); // warm up the CPU 38 | const js1 = test(ssbKeys); 39 | const ne1 = test(ssbKeysNeon); 40 | const js2 = test(ssbKeys); 41 | const ne2 = test(ssbKeysNeon); 42 | const js3 = test(ssbKeys); 43 | const ne3 = test(ssbKeysNeon); 44 | const ssbKeysDuration = Math.round((js1 + js2 + js3) / 3); 45 | const ssbKeysNeonDuration = Math.round((ne1 + ne2 + ne3) / 3); 46 | const speedup = ((100 * ssbKeysNeonDuration) / ssbKeysDuration).toFixed(1); 47 | console.log(`ssb-keys ran in ${ssbKeysDuration}ms`); 48 | console.log(`ssb-neon-keys ran in ${ssbKeysNeonDuration}ms (${speedup}%)`); 49 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('neon-load-or-build')({ 2 | moduleName: 'ssb-keys-neon', 3 | dir: __dirname + '/..', 4 | }); 5 | -------------------------------------------------------------------------------- /native/.rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 -------------------------------------------------------------------------------- /native/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aead" 7 | version = "0.3.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" 10 | dependencies = [ 11 | "generic-array", 12 | ] 13 | 14 | [[package]] 15 | name = "aho-corasick" 16 | version = "0.7.6" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" 19 | dependencies = [ 20 | "memchr", 21 | ] 22 | 23 | [[package]] 24 | name = "arrayvec" 25 | version = "0.5.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" 28 | 29 | [[package]] 30 | name = "base64" 31 | version = "0.13.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 34 | 35 | [[package]] 36 | name = "block-buffer" 37 | version = "0.9.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" 40 | dependencies = [ 41 | "generic-array", 42 | ] 43 | 44 | [[package]] 45 | name = "byteorder" 46 | version = "1.3.4" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 49 | 50 | [[package]] 51 | name = "cc" 52 | version = "1.0.50" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" 55 | 56 | [[package]] 57 | name = "cfg-if" 58 | version = "0.1.10" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 61 | 62 | [[package]] 63 | name = "cfg-if" 64 | version = "1.0.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 67 | 68 | [[package]] 69 | name = "cipher" 70 | version = "0.2.5" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" 73 | dependencies = [ 74 | "generic-array", 75 | ] 76 | 77 | [[package]] 78 | name = "cpuid-bool" 79 | version = "0.1.2" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" 82 | 83 | [[package]] 84 | name = "crypto-mac" 85 | version = "0.10.0" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6" 88 | dependencies = [ 89 | "generic-array", 90 | "subtle", 91 | ] 92 | 93 | [[package]] 94 | name = "curve25519-dalek" 95 | version = "3.0.0" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "c8492de420e9e60bc9a1d66e2dbb91825390b738a388606600663fc529b4b307" 98 | dependencies = [ 99 | "byteorder", 100 | "digest", 101 | "rand_core", 102 | "subtle", 103 | "zeroize", 104 | ] 105 | 106 | [[package]] 107 | name = "digest" 108 | version = "0.9.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" 111 | dependencies = [ 112 | "generic-array", 113 | ] 114 | 115 | [[package]] 116 | name = "ed25519" 117 | version = "1.0.2" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "07dfc993ea376e864fe29a4099a61ca0bb994c6d7745a61bf60ddb3d64e05237" 120 | dependencies = [ 121 | "signature", 122 | ] 123 | 124 | [[package]] 125 | name = "ed25519-dalek" 126 | version = "1.0.1" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" 129 | dependencies = [ 130 | "curve25519-dalek", 131 | "ed25519", 132 | "rand", 133 | "sha2", 134 | "zeroize", 135 | ] 136 | 137 | [[package]] 138 | name = "generic-array" 139 | version = "0.14.4" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" 142 | dependencies = [ 143 | "typenum", 144 | "version_check", 145 | ] 146 | 147 | [[package]] 148 | name = "getrandom" 149 | version = "0.1.15" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" 152 | dependencies = [ 153 | "cfg-if 0.1.10", 154 | "libc", 155 | "wasi", 156 | ] 157 | 158 | [[package]] 159 | name = "hmac" 160 | version = "0.10.1" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" 163 | dependencies = [ 164 | "crypto-mac", 165 | "digest", 166 | ] 167 | 168 | [[package]] 169 | name = "itoa" 170 | version = "0.4.5" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" 173 | 174 | [[package]] 175 | name = "libc" 176 | version = "0.2.66" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" 179 | 180 | [[package]] 181 | name = "memchr" 182 | version = "2.3.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "3197e20c7edb283f87c071ddfc7a2cca8f8e0b888c242959846a6fce03c72223" 185 | 186 | [[package]] 187 | name = "neon" 188 | version = "0.10.1" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "28e15415261d880aed48122e917a45e87bb82cf0260bb6db48bbab44b7464373" 191 | dependencies = [ 192 | "neon-build 0.10.1", 193 | "neon-runtime", 194 | "semver", 195 | "smallvec", 196 | ] 197 | 198 | [[package]] 199 | name = "neon-build" 200 | version = "0.5.1" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "6a47d5ac62595f747bf4c41b533b8af0cf7ed275745c0378e623169c07c3dc92" 203 | dependencies = [ 204 | "cfg-if 0.1.10", 205 | ] 206 | 207 | [[package]] 208 | name = "neon-build" 209 | version = "0.10.1" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "8bac98a702e71804af3dacfde41edde4a16076a7bbe889ae61e56e18c5b1c811" 212 | dependencies = [ 213 | "neon-sys", 214 | ] 215 | 216 | [[package]] 217 | name = "neon-runtime" 218 | version = "0.10.1" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "4676720fa8bb32c64c3d9f49c47a47289239ec46b4bdb66d0913cc512cb0daca" 221 | dependencies = [ 222 | "cfg-if 1.0.0", 223 | "neon-sys", 224 | "smallvec", 225 | ] 226 | 227 | [[package]] 228 | name = "neon-sys" 229 | version = "0.10.1" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "a5ebc923308ac557184455b4aaa749470554cbac70eb4daa8b18cdc16bef7df6" 232 | dependencies = [ 233 | "cc", 234 | "regex", 235 | ] 236 | 237 | [[package]] 238 | name = "once_cell" 239 | version = "1.12.0" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" 242 | 243 | [[package]] 244 | name = "opaque-debug" 245 | version = "0.3.0" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 248 | 249 | [[package]] 250 | name = "poly1305" 251 | version = "0.6.0" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "d9b42192ab143ed7619bf888a7f9c6733a9a2153b218e2cd557cfdb52fbf9bb1" 254 | dependencies = [ 255 | "universal-hash", 256 | ] 257 | 258 | [[package]] 259 | name = "ppv-lite86" 260 | version = "0.2.9" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" 263 | 264 | [[package]] 265 | name = "private-box" 266 | version = "0.6.0" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "edd373b34a8efae8c2aab2a203ef95a4af8858a249e52fe97d76f6c67e39a135" 269 | dependencies = [ 270 | "ssb-crypto", 271 | "zerocopy", 272 | ] 273 | 274 | [[package]] 275 | name = "proc-macro2" 276 | version = "1.0.24" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" 279 | dependencies = [ 280 | "unicode-xid", 281 | ] 282 | 283 | [[package]] 284 | name = "quote" 285 | version = "1.0.2" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" 288 | dependencies = [ 289 | "proc-macro2", 290 | ] 291 | 292 | [[package]] 293 | name = "rand" 294 | version = "0.7.3" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 297 | dependencies = [ 298 | "getrandom", 299 | "rand_chacha", 300 | "rand_core", 301 | "rand_hc", 302 | ] 303 | 304 | [[package]] 305 | name = "rand_chacha" 306 | version = "0.2.2" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 309 | dependencies = [ 310 | "ppv-lite86", 311 | "rand_core", 312 | ] 313 | 314 | [[package]] 315 | name = "rand_core" 316 | version = "0.5.1" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 319 | dependencies = [ 320 | "getrandom", 321 | ] 322 | 323 | [[package]] 324 | name = "rand_hc" 325 | version = "0.2.0" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 328 | dependencies = [ 329 | "rand_core", 330 | ] 331 | 332 | [[package]] 333 | name = "regex" 334 | version = "1.3.3" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "b5508c1941e4e7cb19965abef075d35a9a8b5cdf0846f30b4050e9b55dc55e87" 337 | dependencies = [ 338 | "aho-corasick", 339 | "memchr", 340 | "regex-syntax", 341 | "thread_local", 342 | ] 343 | 344 | [[package]] 345 | name = "regex-syntax" 346 | version = "0.6.13" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "e734e891f5b408a29efbf8309e656876276f49ab6a6ac208600b4419bd893d90" 349 | 350 | [[package]] 351 | name = "ryu" 352 | version = "1.0.2" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" 355 | 356 | [[package]] 357 | name = "salsa20" 358 | version = "0.7.2" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "399f290ffc409596022fce5ea5d4138184be4784f2b28c62c59f0d8389059a15" 361 | dependencies = [ 362 | "cipher", 363 | "zeroize", 364 | ] 365 | 366 | [[package]] 367 | name = "semver" 368 | version = "0.9.0" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" 371 | dependencies = [ 372 | "semver-parser", 373 | ] 374 | 375 | [[package]] 376 | name = "semver-parser" 377 | version = "0.7.0" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" 380 | 381 | [[package]] 382 | name = "serde" 383 | version = "1.0.118" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" 386 | dependencies = [ 387 | "serde_derive", 388 | ] 389 | 390 | [[package]] 391 | name = "serde_derive" 392 | version = "1.0.118" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" 395 | dependencies = [ 396 | "proc-macro2", 397 | "quote", 398 | "syn", 399 | ] 400 | 401 | [[package]] 402 | name = "serde_json" 403 | version = "1.0.60" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779" 406 | dependencies = [ 407 | "itoa", 408 | "ryu", 409 | "serde", 410 | ] 411 | 412 | [[package]] 413 | name = "sha2" 414 | version = "0.9.2" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "6e7aab86fe2149bad8c507606bdb3f4ef5e7b2380eb92350f56122cca72a42a8" 417 | dependencies = [ 418 | "block-buffer", 419 | "cfg-if 1.0.0", 420 | "cpuid-bool", 421 | "digest", 422 | "opaque-debug", 423 | ] 424 | 425 | [[package]] 426 | name = "signature" 427 | version = "1.2.2" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "29f060a7d147e33490ec10da418795238fd7545bba241504d6b31a409f2e6210" 430 | 431 | [[package]] 432 | name = "smallvec" 433 | version = "1.8.0" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" 436 | 437 | [[package]] 438 | name = "ssb-crypto" 439 | version = "0.2.3" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "a055e461165ec87134bb33a521e7bb4e49f306ed14db31cab221dc649b2762a2" 442 | dependencies = [ 443 | "base64", 444 | "curve25519-dalek", 445 | "ed25519-dalek", 446 | "hmac", 447 | "rand", 448 | "sha2", 449 | "subtle", 450 | "x25519-dalek", 451 | "xsalsa20poly1305", 452 | "zerocopy", 453 | "zeroize", 454 | ] 455 | 456 | [[package]] 457 | name = "ssb-keyfile" 458 | version = "0.5.4" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "8dc0b4660dcbc0f17195e012306eb1cb80e6d4037458e93529a81a4a1240504d" 461 | dependencies = [ 462 | "base64", 463 | "serde", 464 | "serde_json", 465 | "ssb-crypto", 466 | "thiserror", 467 | ] 468 | 469 | [[package]] 470 | name = "ssb-keys-neon" 471 | version = "8.0.0" 472 | dependencies = [ 473 | "arrayvec", 474 | "base64", 475 | "neon", 476 | "neon-build 0.5.1", 477 | "private-box", 478 | "ssb-crypto", 479 | "ssb-keyfile", 480 | ] 481 | 482 | [[package]] 483 | name = "subtle" 484 | version = "2.4.0" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" 487 | 488 | [[package]] 489 | name = "syn" 490 | version = "1.0.54" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44" 493 | dependencies = [ 494 | "proc-macro2", 495 | "quote", 496 | "unicode-xid", 497 | ] 498 | 499 | [[package]] 500 | name = "synstructure" 501 | version = "0.12.4" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" 504 | dependencies = [ 505 | "proc-macro2", 506 | "quote", 507 | "syn", 508 | "unicode-xid", 509 | ] 510 | 511 | [[package]] 512 | name = "thiserror" 513 | version = "1.0.22" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "0e9ae34b84616eedaaf1e9dd6026dbe00dcafa92aa0c8077cb69df1fcfe5e53e" 516 | dependencies = [ 517 | "thiserror-impl", 518 | ] 519 | 520 | [[package]] 521 | name = "thiserror-impl" 522 | version = "1.0.22" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56" 525 | dependencies = [ 526 | "proc-macro2", 527 | "quote", 528 | "syn", 529 | ] 530 | 531 | [[package]] 532 | name = "thread_local" 533 | version = "1.1.4" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" 536 | dependencies = [ 537 | "once_cell", 538 | ] 539 | 540 | [[package]] 541 | name = "typenum" 542 | version = "1.12.0" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" 545 | 546 | [[package]] 547 | name = "unicode-xid" 548 | version = "0.2.0" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 551 | 552 | [[package]] 553 | name = "universal-hash" 554 | version = "0.4.0" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" 557 | dependencies = [ 558 | "generic-array", 559 | "subtle", 560 | ] 561 | 562 | [[package]] 563 | name = "version_check" 564 | version = "0.9.2" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" 567 | 568 | [[package]] 569 | name = "wasi" 570 | version = "0.9.0+wasi-snapshot-preview1" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 573 | 574 | [[package]] 575 | name = "x25519-dalek" 576 | version = "1.1.0" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "bc614d95359fd7afc321b66d2107ede58b246b844cf5d8a0adcca413e439f088" 579 | dependencies = [ 580 | "curve25519-dalek", 581 | "rand_core", 582 | "zeroize", 583 | ] 584 | 585 | [[package]] 586 | name = "xsalsa20poly1305" 587 | version = "0.6.0" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "0304c336e98d753428f7b3d8899d60b8a87a961ef50bdfc44af0c1bea2651ce5" 590 | dependencies = [ 591 | "aead", 592 | "poly1305", 593 | "salsa20", 594 | "subtle", 595 | "zeroize", 596 | ] 597 | 598 | [[package]] 599 | name = "zerocopy" 600 | version = "0.3.0" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "6580539ad917b7c026220c4b3f2c08d52ce54d6ce0dc491e66002e35388fab46" 603 | dependencies = [ 604 | "byteorder", 605 | "zerocopy-derive", 606 | ] 607 | 608 | [[package]] 609 | name = "zerocopy-derive" 610 | version = "0.2.0" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "d498dbd1fd7beb83c86709ae1c33ca50942889473473d287d56ce4770a18edfb" 613 | dependencies = [ 614 | "proc-macro2", 615 | "syn", 616 | "synstructure", 617 | ] 618 | 619 | [[package]] 620 | name = "zeroize" 621 | version = "1.2.0" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "81a974bcdd357f0dca4d41677db03436324d45a4c9ed2d0b873a5a360ce41c36" 624 | dependencies = [ 625 | "zeroize_derive", 626 | ] 627 | 628 | [[package]] 629 | name = "zeroize_derive" 630 | version = "1.0.1" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "c3f369ddb18862aba61aa49bf31e74d29f0f162dec753063200e1dc084345d16" 633 | dependencies = [ 634 | "proc-macro2", 635 | "quote", 636 | "syn", 637 | "synstructure", 638 | ] 639 | -------------------------------------------------------------------------------- /native/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ssb-keys-neon" 3 | version = "8.0.0" 4 | authors = ["Andre Staltz "] 5 | license = "LGPL-3.0" 6 | build = "build.rs" 7 | exclude = ["artifacts.json", "index.node"] 8 | edition = "2018" 9 | 10 | [lib] 11 | name = "ssb_neon_keys" 12 | crate-type = ["cdylib"] 13 | 14 | [build-dependencies] 15 | neon-build = "0.5.1" 16 | 17 | [dependencies] 18 | neon = "0.10.1" 19 | ssb-keyfile = "0.5.4" 20 | ssb-crypto = "0.2.3" 21 | private-box = "0.6.0" 22 | base64 = "0.13.0" 23 | arrayvec = "0.5.1" 24 | -------------------------------------------------------------------------------- /native/build.rs: -------------------------------------------------------------------------------- 1 | extern crate neon_build; 2 | 3 | fn main() { 4 | neon_build::setup(); // must be called in build.rs 5 | 6 | // add project-specific build logic here... 7 | } 8 | -------------------------------------------------------------------------------- /native/src/generate.rs: -------------------------------------------------------------------------------- 1 | use super::utils::{make_keys_obj, HandleExt, OptionExt}; 2 | use neon::prelude::*; 3 | use ssb_crypto::Keypair; 4 | 5 | pub fn neon_generate(mut cx: FunctionContext) -> JsResult { 6 | let args_length = cx.len(); 7 | if args_length == 0 { 8 | let keypair = Keypair::generate(); 9 | return make_keys_obj(&mut cx, &keypair); 10 | } 11 | 12 | // First argument: curve (default = "ed25519") 13 | let curve = if let Some(s) = cx 14 | .argument::(0) 15 | .unwrap() 16 | .try_downcast::() 17 | { 18 | s.value() 19 | } else { 20 | "ed25519".to_string() 21 | }; 22 | 23 | // The only valid curve types: ['ed25519'] 24 | if curve != "ed25519" { 25 | return cx.throw_error("curve argument only supports: ed25519"); 26 | } 27 | 28 | // Second argument: seed 29 | let maybe_seed = cx 30 | .argument_opt(1) 31 | .map(|v| { 32 | v.try_downcast::() 33 | .or_throw(&mut cx, "seed argument must be a buffer") 34 | }) 35 | .transpose()?; 36 | 37 | // Use seed if given, else, generate from random 38 | let keypair = match maybe_seed { 39 | Some(seed_buffer) => { 40 | let seed_bytes = cx.borrow(&seed_buffer, |data| data.as_slice::()); 41 | Keypair::from_seed(&seed_bytes).or_throw(&mut cx, "seed buffer must be 32 bytes") 42 | } 43 | None => Ok(Keypair::generate()), 44 | }?; 45 | 46 | make_keys_obj(&mut cx, &keypair) 47 | } 48 | -------------------------------------------------------------------------------- /native/src/hash.rs: -------------------------------------------------------------------------------- 1 | use super::utils::{self, StringExt}; 2 | use arrayvec::ArrayVec; 3 | use neon::prelude::*; 4 | use ssb_crypto::hash; 5 | 6 | pub fn neon_hash(mut cx: FunctionContext) -> JsResult { 7 | let args_length = cx.len(); 8 | 9 | let data = cx.argument::(0)?; 10 | if !(data.is_a::() || data.is_a::()) { 11 | return cx.throw_error("expected 1st argument to `hash` to be a string or buffer"); 12 | } 13 | 14 | let enc = { 15 | let fallback = cx.string("binary").upcast::(); 16 | if args_length == 2 { 17 | cx.argument::(1).and_then(|v| { 18 | if v.is_a::() { 19 | Ok(v) 20 | } else if v.is_a::() || v.is_a::() { 21 | Ok(fallback) 22 | } else { 23 | cx.throw_error("expected encoding string as the 2nd argument to `hash`") 24 | } 25 | }) 26 | } else { 27 | Ok(fallback) 28 | } 29 | }?; 30 | 31 | let data_buffer = utils::buffer_from(&mut cx, ArrayVec::from([data, enc]))?; 32 | let data_bytes = cx.borrow(&data_buffer, |bytes| bytes.as_slice::()); 33 | 34 | Ok(cx.string(hash(data_bytes).as_base64().with_suffix(".sha256"))) 35 | } 36 | -------------------------------------------------------------------------------- /native/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod generate; 2 | mod hash; 3 | mod load_create; 4 | mod misc; 5 | mod secret; 6 | mod sig; 7 | mod unbox; 8 | mod utils; 9 | 10 | use self::generate::neon_generate; 11 | use self::hash::neon_hash; 12 | use self::load_create::{ 13 | neon_create, neon_create_sync, neon_load, neon_load_or_create, neon_load_or_create_sync, 14 | neon_load_sync, 15 | }; 16 | use self::misc::neon_get_tag; 17 | use self::secret::{neon_secret_box, neon_secret_unbox}; 18 | use self::sig::{neon_sign, neon_sign_obj, neon_verify, neon_verify_obj}; 19 | use self::unbox::{neon_box, neon_sk_to_curve, neon_unbox, neon_unbox_body, neon_unbox_key}; 20 | use neon::prelude::*; 21 | 22 | register_module!(mut cx, { 23 | cx.export_function("generate", neon_generate)?; 24 | cx.export_function("load", neon_load)?; 25 | cx.export_function("loadSync", neon_load_sync)?; 26 | cx.export_function("create", neon_create)?; 27 | cx.export_function("createSync", neon_create_sync)?; 28 | cx.export_function("loadOrCreate", neon_load_or_create)?; 29 | cx.export_function("loadOrCreateSync", neon_load_or_create_sync)?; 30 | cx.export_function("signObj", neon_sign_obj)?; 31 | cx.export_function("verifyObj", neon_verify_obj)?; 32 | cx.export_function("sign", neon_sign)?; 33 | cx.export_function("verify", neon_verify)?; 34 | cx.export_function("getTag", neon_get_tag)?; 35 | cx.export_function("hash", neon_hash)?; 36 | cx.export_function("box", neon_box)?; 37 | cx.export_function("unbox", neon_unbox)?; 38 | cx.export_function("unboxKey", neon_unbox_key)?; 39 | cx.export_function("unboxBody", neon_unbox_body)?; 40 | cx.export_function("ssbSecretKeyToPrivateBoxSecret", neon_sk_to_curve)?; 41 | cx.export_function("secretBox", neon_secret_box)?; 42 | cx.export_function("secretUnbox", neon_secret_unbox)?; 43 | Ok(()) 44 | }); 45 | -------------------------------------------------------------------------------- /native/src/load_create.rs: -------------------------------------------------------------------------------- 1 | use super::utils::{make_keys_obj, ContextExt}; 2 | use neon::prelude::*; 3 | 4 | use ssb_crypto::Keypair; 5 | use ssb_keyfile::KeyFileError as SSBError; 6 | 7 | use std::io::Error; 8 | use std::path::Path; 9 | 10 | fn internal_create>(path: P) -> Result { 11 | if path.as_ref().is_dir() { 12 | ssb_keyfile::generate_at_path(path.as_ref().join("secret")) 13 | } else { 14 | ssb_keyfile::generate_at_path(&path) 15 | } 16 | } 17 | 18 | fn internal_load>(path: P) -> Result { 19 | if path.as_ref().is_dir() { 20 | ssb_keyfile::read_from_path(path.as_ref().join("secret")) 21 | } else { 22 | ssb_keyfile::read_from_path(path) 23 | } 24 | } 25 | 26 | struct CreateTask { 27 | argument: String, 28 | } 29 | 30 | impl Task for CreateTask { 31 | type Output = Keypair; 32 | type Error = Error; 33 | type JsEvent = JsObject; 34 | 35 | fn perform(&self) -> Result { 36 | internal_create(&self.argument) 37 | } 38 | 39 | fn complete(self, mut cx: TaskContext, result: Result) -> JsResult { 40 | let keypair = result.or_else(|e| cx.throw_error(e.to_string()))?; 41 | 42 | make_keys_obj(&mut cx, &keypair) 43 | } 44 | } 45 | 46 | struct LoadTask { 47 | argument: String, 48 | } 49 | 50 | impl Task for LoadTask { 51 | type Output = Keypair; 52 | type Error = SSBError; 53 | type JsEvent = JsObject; 54 | 55 | fn perform(&self) -> Result { 56 | internal_load(&self.argument) 57 | } 58 | 59 | fn complete(self, mut cx: TaskContext, result: Result) -> JsResult { 60 | let keypair = result.or_else(|e| cx.throw_error(e.to_string()))?; 61 | 62 | make_keys_obj(&mut cx, &keypair) 63 | } 64 | } 65 | 66 | struct LoadOrCreateTask { 67 | argument: String, 68 | } 69 | 70 | impl Task for LoadOrCreateTask { 71 | type Output = Keypair; 72 | type Error = Error; 73 | type JsEvent = JsObject; 74 | 75 | fn perform(&self) -> Result { 76 | internal_load(&self.argument).or_else(|_| internal_create(&self.argument)) 77 | } 78 | 79 | fn complete(self, mut cx: TaskContext, result: Result) -> JsResult { 80 | let keypair = result.or_else(|e| cx.throw_error(e.to_string()))?; 81 | 82 | make_keys_obj(&mut cx, &keypair) 83 | } 84 | } 85 | 86 | pub fn neon_create(mut cx: FunctionContext) -> JsResult { 87 | // TODO support all arguments (path, curve, isLegacy, cb) 88 | let path = cx 89 | .arg_as::(0, "expected string as the first argument to `create`")? 90 | .value(); 91 | 92 | let cb = cx.arg_as::(1, "expected a callback function given to `create`")?; 93 | let task = CreateTask { argument: path }; 94 | task.schedule(cb); 95 | Ok(cx.undefined()) 96 | } 97 | 98 | pub fn neon_create_sync(mut cx: FunctionContext) -> JsResult { 99 | // TODO support all arguments (path, curve, isLegacy) 100 | let path = cx 101 | .arg_as::(0, "expected string as only argument to `loadSync`")? 102 | .value(); 103 | 104 | let keypair = internal_create(&path).or_else(|e| cx.throw_error(e.to_string()))?; 105 | 106 | make_keys_obj(&mut cx, &keypair) 107 | } 108 | 109 | pub fn neon_load(mut cx: FunctionContext) -> JsResult { 110 | let path = cx 111 | .arg_as::(0, "expected string as the first argument to `load`")? 112 | .value(); 113 | 114 | let cb = cx.arg_as::(1, "expected a callback function given to `load`")?; 115 | 116 | let task = LoadTask { argument: path }; 117 | task.schedule(cb); 118 | Ok(cx.undefined()) 119 | } 120 | 121 | pub fn neon_load_sync(mut cx: FunctionContext) -> JsResult { 122 | let path = cx 123 | .arg_as::(0, "expected string as only argument to `loadSync`")? 124 | .value(); 125 | 126 | let keypair = internal_load(&path).or_else(|e| cx.throw_error(e.to_string()))?; 127 | 128 | make_keys_obj(&mut cx, &keypair) 129 | } 130 | 131 | pub fn neon_load_or_create(mut cx: FunctionContext) -> JsResult { 132 | let path = cx 133 | .arg_as::(0, "expected string as the first argument to `loadOrCreate`")? 134 | .value(); 135 | 136 | let cb = cx.arg_as::(1, "expected a callback function given to `loadOrCreate`")?; 137 | 138 | let task = LoadOrCreateTask { argument: path }; 139 | task.schedule(cb); 140 | Ok(cx.undefined()) 141 | } 142 | 143 | pub fn neon_load_or_create_sync(mut cx: FunctionContext) -> JsResult { 144 | let path = cx 145 | .arg_as::(0, "expected string as only argument to `loadOrCreateSync`")? 146 | .value(); 147 | 148 | let keypair = internal_load(&path) 149 | .or_else(|_| internal_create(&path)) 150 | .or_else(|e| cx.throw_error(e.to_string()))?; 151 | 152 | make_keys_obj(&mut cx, &keypair) 153 | } 154 | -------------------------------------------------------------------------------- /native/src/misc.rs: -------------------------------------------------------------------------------- 1 | use super::utils::ContextExt; 2 | use neon::prelude::*; 3 | 4 | pub fn neon_get_tag(mut cx: FunctionContext) -> JsResult { 5 | let mut input = cx 6 | .arg_as::(0, "expected string as the 1st argument to `getTag`")? 7 | .value(); 8 | let output = input.split_off(input.find('.').unwrap_or(input.len()) + 1); 9 | 10 | Ok(cx.string(output)) 11 | } 12 | -------------------------------------------------------------------------------- /native/src/secret.rs: -------------------------------------------------------------------------------- 1 | use super::utils::{self, ContextExt, OptionExt}; 2 | use arrayvec::ArrayVec; 3 | use neon::prelude::*; 4 | use ssb_crypto::secretbox::{Hmac, Key, Nonce}; 5 | 6 | pub fn neon_secret_box(mut cx: FunctionContext) -> JsResult { 7 | let arg1 = cx.argument::(0)?; 8 | 9 | if arg1.is_a::() || arg1.is_a::() { 10 | return Ok(cx.undefined().upcast()); 11 | } 12 | 13 | let mut plaintext = utils::json_stringify(&mut cx, ArrayVec::from([arg1]))? 14 | .value() 15 | .into_bytes(); 16 | 17 | let js_key = cx.arg_as::(1, "2nd argument must be the key as a buffer")?; 18 | 19 | let key_bytes = cx.borrow(&js_key, |bytes| bytes.as_slice::()); 20 | let key = Key::from_slice(&key_bytes[0..32]) 21 | .or_throw(&mut cx, "expected `secretbox` key to be at least 32 bytes")?; 22 | let nonce = Nonce::from_slice(&key_bytes[0..24]).unwrap(); // infallible 23 | 24 | let hmac = key.seal(&mut plaintext, &nonce); 25 | // `plaintext` now contains the cyphertext. "Attached" format begins with the hmac: 26 | plaintext.splice(0..0, hmac.0.iter().cloned()); 27 | let buffer = utils::bytes_to_buffer(&mut cx, &plaintext)?; 28 | Ok(buffer.upcast()) 29 | } 30 | 31 | pub fn neon_secret_unbox(mut cx: FunctionContext) -> JsResult { 32 | let arg1 = cx.argument::(0)?; 33 | 34 | if !arg1.is_a::() { 35 | return Ok(cx.undefined().upcast()); 36 | } 37 | 38 | let buffer = arg1.downcast::().or_throw(&mut cx)?; 39 | let cyphertext = cx.borrow(&buffer, |bytes| bytes.as_slice::()); 40 | 41 | let js_key = cx.arg_as::(1, "2nd argument must be the key as a buffer")?; 42 | 43 | let key_bytes = cx.borrow(&js_key, |bytes| bytes.as_slice::()); 44 | if key_bytes.len() < 32 { 45 | cx.throw_error("expected `secretbox` key to be at least 32 bytes")?; 46 | } 47 | let key = Key::from_slice(&key_bytes[0..32]).unwrap(); // infallible 48 | let nonce = Nonce::from_slice(&key_bytes[0..24]).unwrap(); // infallible 49 | 50 | let mut plaintext = vec![0; cyphertext.len() - Hmac::SIZE]; 51 | 52 | if !key.open_attached_into(cyphertext, &nonce, &mut plaintext) { 53 | return cx.throw_error("failed to decrypt in secretUnbox"); 54 | } 55 | let plaintext_str = String::from_utf8(plaintext); 56 | if plaintext_str.is_err() { 57 | return Ok(cx.undefined().upcast()); 58 | } 59 | let plaintext_str = cx.string(plaintext_str.unwrap()); 60 | 61 | let out = utils::json_parse(&mut cx, plaintext_str); 62 | if out.is_err() { 63 | return Ok(cx.undefined().upcast()); 64 | } 65 | let out = out.unwrap(); 66 | 67 | Ok(out.upcast()) 68 | } 69 | -------------------------------------------------------------------------------- /native/src/sig.rs: -------------------------------------------------------------------------------- 1 | use super::utils::{ 2 | self, get_string_or_field, type_name, ContextExt, HandleExt, OptionExt, StringExt, ValueExt, 3 | }; 4 | use arrayvec::ArrayVec; 5 | use neon::prelude::*; 6 | 7 | // TODO NetworkKey isn't a great name, I guess 8 | use ssb_crypto::{Keypair, NetworkKey as AuthKey, PublicKey, Signature}; 9 | 10 | // sign: (keys: obj | string, hmac_key: Buffer | string, str: string) => string 11 | pub fn neon_sign(mut cx: FunctionContext) -> JsResult { 12 | // FIXME: detect `curve` from keys.curve or from u.getTag and validate it 13 | let argc = cx.len(); 14 | if argc < 2 { 15 | return cx.throw_error("sign requires at least two arguments: (keys, msg)"); 16 | } 17 | 18 | let keypair = { 19 | let arg = cx.argument(0)?; 20 | let private_str = get_string_or_field(&mut cx, arg, "private").or_throw( 21 | &mut cx, 22 | "expected 1st argument to be the keys object or the private key string", 23 | )?; 24 | 25 | Keypair::from_base64(&private_str).or_throw(&mut cx, "cannot decode private key bytes")? 26 | }; 27 | 28 | // TODO this is exactly the same inside neon_verify_obj, maybe could refactor 29 | let hmac_key = { 30 | if argc == 3 && cx.argument::(1)?.is_truthy(&mut cx) { 31 | let authkey = cx.argument::(1).and_then(|v| { 32 | if let Some(buf) = v.try_downcast::() { 33 | let bytes = cx.borrow(&buf, |data| data.as_slice::()); 34 | AuthKey::from_slice(bytes).or_throw(&mut cx, "hmac_key buffer must be 32 bytes") 35 | } else if let Some(s) = v.try_downcast::() { 36 | AuthKey::from_base64(&s.value()) 37 | .or_throw(&mut cx, "expected 2nd argument to be a base64 string") 38 | } else { 39 | cx.throw_error("expected 2nd argument to be a Buffer for the hmac_key") 40 | } 41 | })?; 42 | Some(authkey) 43 | } else { 44 | None 45 | } 46 | }; 47 | 48 | let msg = cx 49 | .arg_as::(argc - 1, "expected 2nd arg to be a plaintext string")? 50 | .value() 51 | .into_bytes(); 52 | 53 | let sig = match hmac_key { 54 | None => keypair.sign(msg.as_slice()), 55 | Some(hmac_key) => { 56 | let tag = hmac_key.authenticate(msg.as_slice()); 57 | keypair.sign(&tag.0) 58 | } 59 | }; 60 | 61 | let signature = cx.string(sig.as_base64().with_suffix(".sig.ed25519")); 62 | 63 | Ok(signature) 64 | } 65 | 66 | // verify: (keys: obj | string, signature: string, hmac_key, str: string) => boolean 67 | pub fn neon_verify(mut cx: FunctionContext) -> JsResult { 68 | // FIXME: detect `curve` from keys.curve or from u.getTag and validate it 69 | let argc = cx.len(); 70 | if argc < 3 { 71 | return cx.throw_error("verify requires at least two arguments: (keys, msg)"); 72 | } 73 | 74 | let public_key = { 75 | let arg = cx.argument(0)?; 76 | let public_str = get_string_or_field(&mut cx, arg, "public").or_throw( 77 | &mut cx, 78 | "expected `public` argument to be the keys object or the public key string", 79 | )?; 80 | PublicKey::from_base64(&public_str).or_throw(&mut cx, "cannot base64 decode the public key")? 81 | }; 82 | 83 | let signature = { 84 | let mut sig = cx 85 | .arg_as::(1, "expected 2nd arg to be a signature string")? 86 | .value(); 87 | match sig.rfind(".sig.ed25519") { 88 | None => return cx.throw_error("Invalid signature string, is missing dot suffix"), 89 | Some(dot_index) => sig.truncate(dot_index) 90 | }; 91 | Signature::from_base64(&sig).or_throw(&mut cx, "unable to decode signature base64 string")? 92 | }; 93 | 94 | // TODO this is almost the same inside neon_verify_obj, maybe could refactor 95 | let hmac_key = { 96 | if argc == 4 && cx.argument::(2)?.is_truthy(&mut cx) { 97 | let authkey = cx.argument::(2).and_then(|v| { 98 | if let Some(buf) = v.try_downcast::() { 99 | let bytes = cx.borrow(&buf, |data| data.as_slice::()); 100 | AuthKey::from_slice(bytes).or_throw(&mut cx, "hmac_key buffer must be 32 bytes") 101 | } else if let Some(s) = v.try_downcast::() { 102 | AuthKey::from_base64(&s.value()) 103 | .or_throw(&mut cx, "expected 3rd argument to be a base64 string") 104 | } else { 105 | cx.throw_error("expected 3rd argument to be a Buffer for the hmac_key") 106 | } 107 | })?; 108 | Some(authkey) 109 | } else { 110 | None 111 | } 112 | }; 113 | 114 | let msg = cx 115 | .arg_as::(argc - 1 , "expected last arg to be a plaintext string")? 116 | .value(); 117 | 118 | let passed = match hmac_key { 119 | None => public_key.verify(&signature, msg.into_bytes().as_slice()), 120 | Some(hmac_key) => { 121 | let tag = hmac_key.authenticate(msg.into_bytes().as_slice()); 122 | public_key.verify(&signature, &tag.0) 123 | } 124 | }; 125 | 126 | Ok(cx.boolean(passed)) 127 | } 128 | 129 | // sign: (keys: obj | string, hmac_key?: string, o: obj) => string 130 | pub fn neon_sign_obj(mut cx: FunctionContext) -> JsResult { 131 | // FIXME: detect `curve` from keys.curve or from u.getTag and validate it 132 | let argc = cx.len(); 133 | if argc < 2 { 134 | return cx.throw_error("signObj requires at least two arguments: (keys, msg)"); 135 | } 136 | 137 | let keypair = { 138 | let arg = cx.argument(0)?; 139 | let private_str = get_string_or_field(&mut cx, arg, "private").or_throw( 140 | &mut cx, 141 | "expected 1st argument to be the keys object or the private key string", 142 | )?; 143 | 144 | Keypair::from_base64(&private_str).or_throw(&mut cx, "cannot decode private key bytes")? 145 | }; 146 | 147 | // TODO this is exactly the same inside neon_verify_obj, maybe could refactor 148 | let hmac_key = { 149 | if argc == 3 && cx.argument::(1)?.is_truthy(&mut cx) { 150 | let authkey = cx.argument::(1).and_then(|v| { 151 | if let Some(buf) = v.try_downcast::() { 152 | let bytes = cx.borrow(&buf, |data| data.as_slice::()); 153 | AuthKey::from_slice(bytes).or_throw(&mut cx, "hmac_key buffer must be 32 bytes") 154 | } else if let Some(s) = v.try_downcast::() { 155 | AuthKey::from_base64(&s.value()) 156 | .or_throw(&mut cx, "expected 2nd argument to be a base64 string") 157 | } else { 158 | cx.throw_error("expected 2nd argument to be a Buffer for the hmac_key") 159 | } 160 | })?; 161 | Some(authkey) 162 | } else { 163 | None 164 | } 165 | }; 166 | 167 | // TODO this is exactly the same inside neon_verify_obj, maybe could refactor 168 | let out_obj = { 169 | let (index, ord) = if argc == 2 { (1, "2nd") } else { (2, "3rd") }; 170 | let v = cx.argument::(index)?; 171 | let obj = if v.is_a::() { 172 | Ok(v.downcast::().unwrap()) 173 | } else { 174 | cx.throw_error(format!( 175 | "expected {} arg to be object, was a {}", 176 | ord, 177 | type_name(&v) 178 | )) 179 | }?; 180 | utils::clone_js_obj(&mut cx, obj)? 181 | }; 182 | 183 | let msg = { 184 | let null = cx.null(); 185 | let args = ArrayVec::from([out_obj.upcast(), null.upcast(), cx.number(2).upcast()]); 186 | utils::json_stringify(&mut cx, args)?.value().into_bytes() 187 | }; 188 | 189 | // TODO this is exactly the same inside neon_verify_obj, maybe could refactor 190 | let sig = match hmac_key { 191 | None => keypair.sign(msg.as_slice()), 192 | Some(hmac_key) => { 193 | let tag = hmac_key.authenticate(msg.as_slice()); 194 | keypair.sign(&tag.0) 195 | } 196 | }; 197 | let signature = cx.string(sig.as_base64().with_suffix(".sig.ed25519")); 198 | 199 | out_obj 200 | .set(&mut cx, "signature", signature) 201 | .or_else(|_| cx.throw_error("failed to set the `signature` field in the object"))?; 202 | 203 | Ok(out_obj) 204 | } 205 | 206 | // verify: (keys: obj | string, hmac_key?: string, o: obj) => boolean 207 | pub fn neon_verify_obj(mut cx: FunctionContext) -> JsResult { 208 | // FIXME: detect `curve` from keys.curve or from u.getTag and validate it 209 | let argc = cx.len(); 210 | if argc < 2 { 211 | return cx.throw_error("verifyObj requires at least two arguments: (keys, msg)"); 212 | } 213 | 214 | let public_key = { 215 | let arg = cx.argument(0)?; 216 | let public_str = get_string_or_field(&mut cx, arg, "public").or_throw( 217 | &mut cx, 218 | "expected `public` argument to be the keys object or the public key string", 219 | )?; 220 | PublicKey::from_base64(&public_str).or_throw(&mut cx, "cannot base64 decode the public key")? 221 | }; 222 | 223 | let hmac_key = { 224 | if argc == 3 && cx.argument::(1)?.is_truthy(&mut cx) { 225 | let authkey = cx.argument::(1).and_then(|v| { 226 | if let Some(buf) = v.try_downcast::() { 227 | let bytes = cx.borrow(&buf, |data| data.as_slice::()); 228 | AuthKey::from_slice(bytes).or_throw(&mut cx, "hmac_key buffer must be 32 bytes") 229 | } else if let Some(s) = v.try_downcast::() { 230 | AuthKey::from_base64(&s.value()) 231 | .or_throw(&mut cx, "expected 2nd argument to be a base64 string") 232 | } else { 233 | cx.throw_error("expected 2nd argument to be a Buffer for the hmac_key") 234 | } 235 | })?; 236 | Some(authkey) 237 | } else { 238 | None 239 | } 240 | }; 241 | 242 | let verify_obj = { 243 | let (index, ord) = if argc == 2 { (1, "2nd") } else { (2, "3rd") }; 244 | let v = cx.argument::(index)?; 245 | let obj = if v.is_a::() { 246 | Ok(v.downcast::().unwrap()) 247 | } else { 248 | cx.throw_error(format!( 249 | "expected {} arg to be object, was a {}", 250 | ord, 251 | type_name(&v) 252 | )) 253 | }?; 254 | utils::clone_js_obj(&mut cx, obj)? 255 | }; 256 | 257 | let signature = { 258 | let mut sig = verify_obj 259 | .get(&mut cx, "signature") 260 | .or_else(|_| cx.throw_error("obj.signature field is missing from obj"))? 261 | .downcast::() 262 | .or_throw(&mut cx) 263 | .or_else(|_| cx.throw_error("obj.signature field is corrupted or not a string"))? 264 | .value(); 265 | match sig.rfind(".sig.ed25519") { 266 | None => return cx.throw_error("Invalid signature string, is missing dot suffix"), 267 | Some(dot_index) => sig.truncate(dot_index) 268 | }; 269 | Signature::from_base64(&sig).or_throw(&mut cx, "unable to decode signature base64 string")? 270 | }; 271 | 272 | let msg = { 273 | let undef = cx.undefined(); 274 | verify_obj 275 | .set(&mut cx, "signature", undef) // `delete` keyword in JS would be better 276 | .or_else(|_| cx.throw_error("failed to remove the `signature` field from the object"))?; 277 | 278 | let args = ArrayVec::from([ 279 | verify_obj.upcast(), 280 | cx.null().upcast(), 281 | cx.number(2).upcast(), 282 | ]); 283 | utils::json_stringify(&mut cx, args)?.value().into_bytes() 284 | }; 285 | 286 | let passed = match hmac_key { 287 | None => public_key.verify(&signature, msg.as_slice()), 288 | Some(hmac_key) => { 289 | let tag = hmac_key.authenticate(msg.as_slice()); 290 | public_key.verify(&signature, &tag.0) 291 | } 292 | }; 293 | 294 | Ok(cx.boolean(passed)) 295 | } 296 | -------------------------------------------------------------------------------- /native/src/unbox.rs: -------------------------------------------------------------------------------- 1 | use super::utils::{self, get_string_or_field, ContextExt, OptionExt}; 2 | use arrayvec::ArrayVec; 3 | use neon::prelude::*; 4 | use ssb_crypto::ephemeral::sk_to_curve; 5 | use ssb_crypto::{Keypair, PublicKey}; 6 | 7 | pub fn neon_box(mut cx: FunctionContext) -> JsResult { 8 | let arg1 = cx.argument::(0)?; 9 | let msg = utils::json_stringify(&mut cx, ArrayVec::from([arg1]))? 10 | .value() 11 | .into_bytes(); 12 | 13 | let recps: Vec = cx 14 | .arg_as::(1, "expected 2nd argument to be an array of recipients")? 15 | .to_vec(&mut cx)? 16 | .iter() 17 | .flat_map(|recp| { 18 | let public_str = get_string_or_field(&mut cx, *recp, "public").or_throw( 19 | &mut cx, 20 | "each recipient must be a keys object or public key string", 21 | )?; 22 | PublicKey::from_base64(&public_str).or_throw(&mut cx, "cannot base64 decode the public key") 23 | }) 24 | .collect(); 25 | 26 | let multiboxed = private_box::encrypt(msg.as_slice(), recps.as_slice()); 27 | let mut out = base64::encode_config(multiboxed.as_slice(), base64::STANDARD); 28 | out.push_str(".box"); 29 | 30 | Ok(cx.string(out)) 31 | } 32 | 33 | pub fn neon_unbox(mut cx: FunctionContext) -> JsResult { 34 | let cyphertext = { 35 | let ctxt_str = cx 36 | .arg_as::(0, "expected 1st argument to be the cyphertext as a string")? 37 | .value(); 38 | base64::decode_config(ctxt_str.trim_end_matches(".box"), base64::STANDARD) 39 | }; 40 | if cyphertext.is_err() { 41 | return Ok(cx.undefined().upcast()); 42 | } 43 | let cyphertext = cyphertext.unwrap(); 44 | 45 | let private_key = { 46 | let v = cx.argument(1)?; 47 | let private_str = get_string_or_field(&mut cx, v, "private").or_throw( 48 | &mut cx, 49 | "expected 2nd argument to be the keys object or the private key string", 50 | )?; 51 | Keypair::from_base64(&private_str).or_throw( 52 | &mut cx, 53 | "cannot base64 decode the private key given to `signObj`", 54 | )? 55 | }; 56 | 57 | let msg = private_box::decrypt(cyphertext.as_slice(), &private_key).ok_or(0); 58 | if msg.is_err() { 59 | return Ok(cx.undefined().upcast()); 60 | } 61 | let msg = msg.unwrap(); 62 | let msg_str = String::from_utf8(msg); 63 | if msg_str.is_err() { 64 | return Ok(cx.undefined().upcast()); 65 | } 66 | let msg_str = cx.string(msg_str.unwrap()); 67 | 68 | let out = utils::json_parse(&mut cx, msg_str); 69 | if out.is_err() { 70 | return Ok(cx.undefined().upcast()); 71 | } 72 | let out = out.unwrap(); 73 | 74 | Ok(out.upcast()) 75 | } 76 | 77 | pub fn neon_unbox_key(mut cx: FunctionContext) -> JsResult { 78 | let cyphertext = { 79 | let ctxt_str = cx 80 | .arg_as::(0, "expected 1st argument to be the cyphertext as a string")? 81 | .value(); 82 | base64::decode_config(ctxt_str.trim_end_matches(".box"), base64::STANDARD) 83 | }; 84 | if cyphertext.is_err() { 85 | return Ok(cx.undefined().upcast()); 86 | } 87 | let cyphertext = cyphertext.unwrap(); 88 | 89 | let keypair = { 90 | let v = cx.argument(1)?; 91 | let private_str = get_string_or_field(&mut cx, v, "private").or_throw( 92 | &mut cx, 93 | "expected 2nd argument to be the keys object or the private key string", 94 | )?; 95 | Keypair::from_base64(&private_str).or_throw( 96 | &mut cx, 97 | "cannot base64 decode the private key given to `signObj`", 98 | )? 99 | }; 100 | 101 | let opened_key = private_box::decrypt_key(&cyphertext, &keypair); 102 | if opened_key.is_none() { 103 | return Ok(cx.undefined().upcast()); 104 | } 105 | 106 | let buffer = utils::bytes_to_buffer(&mut cx, &opened_key.unwrap().as_array())?; 107 | Ok(buffer.upcast()) 108 | } 109 | 110 | // TODO should also allow JsBuffer ciphertext 111 | pub fn neon_unbox_body(mut cx: FunctionContext) -> JsResult { 112 | let cyphertext = { 113 | let ctxt_str = cx 114 | .arg_as::(0, "expected 1st argument to be the cyphertext as a string")? 115 | .value(); 116 | base64::decode_config(ctxt_str.trim_end_matches(".box"), base64::STANDARD) 117 | }; 118 | if cyphertext.is_err() { 119 | return Ok(cx.undefined().upcast()); 120 | } 121 | let cyphertext = cyphertext.unwrap(); 122 | 123 | let opened_key_buf = 124 | cx.arg_as::(1, "expected 2nd argument to be a buffer for the opened key")?; 125 | let opened_key = cx.borrow(&opened_key_buf, |data| data.as_slice::()); 126 | 127 | let msg = private_box::decrypt_body_with_key_bytes(&cyphertext, &opened_key); 128 | 129 | if msg.is_none() { 130 | return Ok(cx.undefined().upcast()); 131 | } 132 | let msg_str = String::from_utf8(msg.unwrap()); 133 | if msg_str.is_err() { 134 | return Ok(cx.undefined().upcast()); 135 | } 136 | let msg_str = cx.string(msg_str.unwrap()); 137 | 138 | let out = utils::json_parse(&mut cx, msg_str); 139 | if out.is_err() { 140 | return Ok(cx.undefined().upcast()); 141 | } 142 | let out = out.unwrap(); 143 | 144 | Ok(out.upcast()) 145 | } 146 | 147 | // ssbSecretKeyToPrivateBoxSecret 148 | pub fn neon_sk_to_curve(mut cx: FunctionContext) -> JsResult { 149 | let keypair = { 150 | let v = cx.argument(0)?; 151 | let private_str = get_string_or_field(&mut cx, v, "private").or_throw( 152 | &mut cx, 153 | "expected 1st argument to be the keys object or the private key string", 154 | )?; 155 | 156 | Keypair::from_base64(&private_str).or_throw( 157 | &mut cx, 158 | "cannot base64 decode the private key given to `signObj`", 159 | )? 160 | }; 161 | 162 | let curve = sk_to_curve(&keypair.secret); 163 | if curve.is_none() { 164 | return cx.throw_error("failed to run ssbSecretKeyToPrivateBoxSecret"); 165 | } 166 | 167 | let buffer = utils::bytes_to_buffer(&mut cx, &curve.unwrap().0)?; 168 | Ok(buffer.upcast()) 169 | } 170 | -------------------------------------------------------------------------------- /native/src/utils.rs: -------------------------------------------------------------------------------- 1 | use arrayvec::ArrayVec; 2 | use neon::handle::Managed; 3 | use neon::object::This; 4 | use neon::prelude::*; 5 | use ssb_crypto::Keypair; 6 | 7 | pub fn make_keys_obj<'a>(cx: &mut impl Context<'a>, kp: &Keypair) -> JsResult<'a, JsObject> { 8 | let keys_obj = JsObject::new(cx); 9 | let curve_val = cx.string("ed25519"); 10 | let id_val = cx.string(kp.public.as_base64().wrap('@', ".ed25519")); 11 | let private_val = cx.string(kp.as_base64().with_suffix(".ed25519")); 12 | let public_val = cx.string(kp.public.as_base64().with_suffix(".ed25519")); 13 | keys_obj.set(cx, "curve", curve_val)?; 14 | keys_obj.set(cx, "id", id_val)?; 15 | keys_obj.set(cx, "private", private_val)?; 16 | keys_obj.set(cx, "public", public_val)?; 17 | Ok(keys_obj) 18 | } 19 | 20 | pub fn call_builtin<'a, T>( 21 | cx: &mut impl Context<'a>, 22 | module: &str, 23 | name: &str, 24 | args: impl IntoIterator>, 25 | ) -> JsResult<'a, T> 26 | where 27 | T: Value + Managed, 28 | { 29 | let func = cx 30 | .global() 31 | .get(cx, module)? 32 | .downcast::() 33 | .or_throw(cx)? 34 | .get(cx, name)? 35 | .downcast::() 36 | .or_throw(cx)?; 37 | 38 | let null = cx.null(); 39 | func.call(cx, null, args)?.downcast::().or_throw(cx) 40 | } 41 | 42 | // TODO publish to some neon-helpers library? 43 | pub fn json_stringify<'a>( 44 | cx: &mut impl Context<'a>, 45 | args: impl IntoIterator>, 46 | ) -> JsResult<'a, JsString> { 47 | call_builtin(cx, "JSON", "stringify", args) 48 | } 49 | 50 | pub fn json_parse<'a>( 51 | cx: &mut impl Context<'a>, 52 | arg: Handle<'a, JsString>, 53 | ) -> JsResult<'a, JsObject> { 54 | call_builtin(cx, "JSON", "parse", ArrayVec::from([arg.upcast()])) 55 | } 56 | 57 | pub fn buffer_from<'a>( 58 | cx: &mut impl Context<'a>, 59 | args: impl IntoIterator>, 60 | ) -> JsResult<'a, JsBuffer> { 61 | call_builtin(cx, "Buffer", "from", args) 62 | } 63 | 64 | // TODO publish to some neon-helpers library? 65 | pub fn clone_js_obj<'a>( 66 | cx: &mut impl Context<'a>, 67 | obj: Handle, 68 | ) -> JsResult<'a, JsObject> { 69 | let new_obj = cx.empty_object(); 70 | let keys = obj.get_own_property_names(cx)?; 71 | for i in 0..keys.len() { 72 | let key = keys 73 | .get(cx, i)? 74 | .downcast::() 75 | .or_throw(cx)? 76 | .value(); 77 | let val = obj.get(cx, key.as_str())?; 78 | new_obj.set(cx, key.as_str(), val)?; 79 | } 80 | Ok(new_obj) 81 | } 82 | 83 | pub fn bytes_to_buffer<'a>(cx: &mut impl Context<'a>, bytes: &[u8]) -> JsResult<'a, JsBuffer> { 84 | let mut buffer = cx.buffer(bytes.len() as u32)?; 85 | cx.borrow_mut(&mut buffer, |data| { 86 | data.as_mut_slice().copy_from_slice(bytes) 87 | }); 88 | Ok(buffer) 89 | } 90 | 91 | pub fn get_string_or_field<'a, T: This>( 92 | cx: &mut CallContext<'a, T>, 93 | v: Handle, 94 | field: &str, 95 | ) -> Option { 96 | if let Some(s) = v.try_downcast::() { 97 | Some(s.value()) 98 | } else if let Some(obj) = v.try_downcast::() { 99 | let f = obj.get(cx, field).ok()?; 100 | let s = f.try_downcast::()?; 101 | Some(s.value()) 102 | } else { 103 | None 104 | } 105 | } 106 | 107 | pub fn type_name(v: &Handle) -> &'static str { 108 | if v.is_a::() { 109 | "array" 110 | } else if v.is_a::() { 111 | "array buffer" 112 | } else if v.is_a::() { 113 | "boolean" 114 | } else if v.is_a::() { 115 | "buffer" 116 | } else if v.is_a::() { 117 | "error" 118 | } else if v.is_a::() { 119 | "null" 120 | } else if v.is_a::() { 121 | "number" 122 | } else if v.is_a::() { 123 | "object" 124 | } else if v.is_a::() { 125 | "string" 126 | } else if v.is_a::() { 127 | "undefined" 128 | } else { 129 | "something else" // :) 130 | } 131 | } 132 | 133 | pub trait StringExt { 134 | fn with_suffix(self, s: &str) -> Self; 135 | fn wrap(self, prefix: char, suffix: &str) -> Self; 136 | } 137 | 138 | impl StringExt for String { 139 | fn with_suffix(mut self, s: &str) -> Self { 140 | self.push_str(s); 141 | self 142 | } 143 | fn wrap(mut self, prefix: char, suffix: &str) -> Self { 144 | self.insert(0, prefix); 145 | self.push_str(suffix); 146 | self 147 | } 148 | } 149 | 150 | pub trait OptionExt { 151 | fn or_throw<'a, S: AsRef>( 152 | self, 153 | cx: &mut impl Context<'a>, 154 | msg: S, 155 | ) -> Result; 156 | } 157 | 158 | impl OptionExt for Option { 159 | fn or_throw<'a, S: AsRef>( 160 | self, 161 | cx: &mut impl Context<'a>, 162 | msg: S, 163 | ) -> Result { 164 | // Result::unwrap_err and expect_err require T: Debug, which JsArray doesn't impl 165 | self.ok_or_else(|| cx.throw_error::<_, T>(msg).err().unwrap()) 166 | } 167 | } 168 | 169 | // `if let Ok(s) = v.downcast::() { ... }` 170 | // can be used with zero cost (aside from the type tag check) 171 | // when this PR is merged: https://github.com/neon-bindings/neon/pull/606 172 | // 173 | // In the meantime, we'll use this: 174 | pub trait HandleExt<'a> { 175 | fn try_downcast(&self) -> Option>; 176 | } 177 | impl<'a, T: Value> HandleExt<'a> for Handle<'a, T> { 178 | fn try_downcast(&self) -> Option> { 179 | if self.is_a::() { 180 | Some(self.downcast::().unwrap()) 181 | } else { 182 | None 183 | } 184 | } 185 | } 186 | 187 | pub trait ValueExt { 188 | fn is_truthy<'a, C: Context<'a>>(&self, cx: &mut C) -> bool; 189 | } 190 | 191 | impl ValueExt for T { 192 | fn is_truthy<'a, C: Context<'a>>(&self, cx: &mut C) -> bool { 193 | let global = cx.global(); 194 | let boolean = global 195 | .get(cx, "Boolean") 196 | .unwrap() 197 | .downcast::() 198 | .unwrap(); 199 | let args = ArrayVec::from([self.as_value(cx)]); 200 | let b = boolean 201 | .call(cx, global, args) 202 | .unwrap() 203 | .downcast::() 204 | .unwrap(); 205 | b.value() 206 | } 207 | } 208 | 209 | // Like `cx.argument::(index)?` but with a custom error msg 210 | pub trait ContextExt<'a> { 211 | fn arg_as( 212 | &mut self, 213 | index: i32, 214 | msg: &str, 215 | ) -> Result, neon::result::Throw>; 216 | } 217 | 218 | impl<'a> ContextExt<'a> for FunctionContext<'a> { 219 | fn arg_as( 220 | &mut self, 221 | index: i32, 222 | msg: &str, 223 | ) -> Result, neon::result::Throw> { 224 | let v = self.argument::(index)?; 225 | v.try_downcast::().or_throw(self, msg) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssb-keys-neon", 3 | "version": "8.2.0-1", 4 | "description": "Rust-in-Node.js variant of ssb-keys", 5 | "main": "lib/index.js", 6 | "author": "Andre Staltz ", 7 | "license": "LGPL-3.0", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/staltz/ssb-keys-neon.git" 11 | }, 12 | "dependencies": { 13 | "neon-cli": "~0.5.1", 14 | "neon-load-or-build": "2.2.2" 15 | }, 16 | "devDependencies": { 17 | "chloride": "2.4.1", 18 | "cpy-cli": "^3.1.1", 19 | "detect-libc": "~1.0.3", 20 | "electron-build-env": "^0.2.0", 21 | "neon-tag-prebuild": "^1.1.0", 22 | "ssb-keys": "8.2.0", 23 | "tape": "^5.2.2" 24 | }, 25 | "scripts": { 26 | "install": "neon-load-or-build", 27 | "build-debug": "neon build", 28 | "build-release": "neon build --release", 29 | "build-release-electron": "electron-build-env neon build --release", 30 | "tag-prebuild": "detect-libc neon-tag-prebuild", 31 | "tag-prebuild-electron": "electron-build-env neon-tag-prebuild", 32 | "setup-compat-tests": "cpy ./node_modules/ssb-keys/test/* ./test-compat", 33 | "pretest": "npm run build-debug", 34 | "test-compat": "npm run setup-compat-tests && tape test-compat/*.js", 35 | "test-extra": "tape test-extra/*.js", 36 | "test": "npm run test-compat && npm run test-extra", 37 | "prepublishOnly": "if [ -z \"$CI\" ]; then echo \"can only npm publish from CI\" && exit 1; fi" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test-extra/index.js: -------------------------------------------------------------------------------- 1 | let tape = require('tape'); 2 | 3 | // This file was used when ssb-keys was lacking tests for some public APIs. 4 | // Nowadays we update ssb-keys tests to cover everything. 5 | // You can still use this file to add ad-hoc tests if you need 6 | 7 | tape('no extra tests', function (t) { 8 | t.end(); 9 | }); 10 | --------------------------------------------------------------------------------