├── .gitignore ├── .github └── workflows │ └── test-node.yml ├── package.json ├── LICENSE ├── README.md ├── test.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | coverage 4 | -------------------------------------------------------------------------------- /.github/workflows/test-node.yml: -------------------------------------------------------------------------------- 1 | name: Build Status 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | node-version: [lts/*] 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm install 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hypercore-crypto", 3 | "version": "3.6.1", 4 | "description": "The crypto primitives used in hypercore, extracted into a separate module", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js" 8 | ], 9 | "dependencies": { 10 | "b4a": "^1.6.6", 11 | "compact-encoding": "^2.15.0", 12 | "sodium-universal": "^5.0.0" 13 | }, 14 | "devDependencies": { 15 | "brittle": "^3.5.0", 16 | "standard": "^17.1.0" 17 | }, 18 | "scripts": { 19 | "test": "standard && brittle test.js" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/mafintosh/hypercore-crypto.git" 24 | }, 25 | "author": "Mathias Buus (@mafintosh)", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/mafintosh/hypercore-crypto/issues" 29 | }, 30 | "homepage": "https://github.com/mafintosh/hypercore-crypto" 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Mathias Buus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hypercore-crypto 2 | 3 | The crypto primitives used in hypercore, extracted into a separate module 4 | 5 | ``` 6 | npm install hypercore-crypto 7 | ``` 8 | 9 | ## Usage 10 | 11 | ``` js 12 | const crypto = require('hypercore-crypto') 13 | 14 | const keyPair = crypto.keyPair() 15 | console.log(keyPair) // prints a ed25519 keypair 16 | ``` 17 | 18 | ## API 19 | 20 | #### `keyPair = crypto.keyPair()` 21 | 22 | Returns an `ED25519` keypair that can be used for tree signing. 23 | 24 | #### `signature = crypto.sign(message, secretKey)` 25 | 26 | Signs a message (buffer). 27 | 28 | #### `verified = crypto.verify(message, signature, publicKey)` 29 | 30 | Verifies a signature for a message. 31 | 32 | #### `hash = crypto.data(data)` 33 | 34 | Hashes a leaf node in a merkle tree. 35 | 36 | #### `hash = crypto.parent(left, right)` 37 | 38 | Hash a parent node in a merkle tree. `left` and `right` should look like this: 39 | 40 | ```js 41 | { 42 | index: treeIndex, 43 | hash: hashOfThisNode, 44 | size: byteSizeOfThisTree 45 | } 46 | ``` 47 | 48 | #### `hash = crypto.tree(peaks)` 49 | 50 | Hashes the merkle root of the tree. `peaks` should be an array of the peaks of the tree and should look like above. 51 | 52 | #### `buffer = crypto.randomBytes(size)` 53 | 54 | Returns a buffer containing random bytes of size `size`. 55 | 56 | #### `hash = crypto.discoveryKey(publicKey)` 57 | 58 | Return a hash derived from a `publicKey` that can used for discovery 59 | without disclosing the public key. 60 | 61 | #### `list = crypto.namespace(name, count)` 62 | 63 | Make a list of namespaces from a specific publicly known name. 64 | Use this to namespace capabilities or hashes / signatures across algorithms. 65 | 66 | ## License 67 | 68 | MIT 69 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('brittle') 2 | const b4a = require('b4a') 3 | const crypto = require('./') 4 | const sodium = require('sodium-universal') 5 | 6 | test('randomBytes', function (t) { 7 | const buffer = crypto.randomBytes(100) 8 | t.ok(b4a.isBuffer(buffer)) 9 | t.unlike(crypto.randomBytes(100), buffer) 10 | }) 11 | 12 | test('key pair', function (t) { 13 | const keyPair = crypto.keyPair() 14 | 15 | t.is(keyPair.publicKey.length, 32) 16 | t.is(keyPair.secretKey.length, 64) 17 | t.is(keyPair.publicKey.buffer.byteLength, 96, 'small slab') 18 | t.is(keyPair.publicKey.buffer, keyPair.secretKey.buffer, 'public and seret key share the same slab') 19 | }) 20 | 21 | test('validate key pair', function (t) { 22 | const keyPair1 = crypto.keyPair() 23 | const keyPair2 = crypto.keyPair() 24 | 25 | t.absent(crypto.validateKeyPair({ publicKey: keyPair1.publicKey, secretKey: keyPair2.secretKey })) 26 | t.ok(crypto.validateKeyPair({ publicKey: keyPair1.publicKey, secretKey: keyPair1.secretKey })) 27 | }) 28 | 29 | test('sign', function (t) { 30 | const keyPair = crypto.keyPair() 31 | const message = b4a.from('hello world') 32 | 33 | const sig = crypto.sign(message, keyPair.secretKey) 34 | 35 | t.is(sig.length, 64) 36 | t.ok(crypto.verify(message, sig, keyPair.publicKey)) 37 | t.absent(crypto.verify(message, b4a.alloc(64), keyPair.publicKey)) 38 | t.is(sig.buffer.byteLength, sodium.crypto_sign_BYTES, 'dedicated slab for signatures') 39 | }) 40 | 41 | test('hash leaf', function (t) { 42 | const data = b4a.from('hello world') 43 | 44 | t.alike(crypto.data(data), b4a.from('9f1b578fd57a4df015493d2886aec9600eef913c3bb009768c7f0fb875996308', 'hex')) 45 | }) 46 | 47 | test('hash parent', function (t) { 48 | const data = b4a.from('hello world') 49 | 50 | const parent = crypto.parent({ 51 | index: 0, 52 | size: 11, 53 | hash: crypto.data(data) 54 | }, { 55 | index: 2, 56 | size: 11, 57 | hash: crypto.data(data) 58 | }) 59 | 60 | t.alike(parent, b4a.from('3ad0c9b58b771d1b7707e1430f37c23a23dd46e0c7c3ab9c16f79d25f7c36804', 'hex')) 61 | }) 62 | 63 | test('tree', function (t) { 64 | const roots = [ 65 | { index: 3, size: 11, hash: b4a.alloc(32) }, 66 | { index: 9, size: 2, hash: b4a.alloc(32) } 67 | ] 68 | 69 | t.alike(crypto.tree(roots), b4a.from('0e576a56b478cddb6ffebab8c494532b6de009466b2e9f7af9143fc54b9eaa36', 'hex')) 70 | }) 71 | 72 | test('hash', function (t) { 73 | const hash1 = b4a.allocUnsafe(32) 74 | const hash2 = b4a.allocUnsafe(32) 75 | const hash3 = b4a.allocUnsafe(32) 76 | 77 | const input = [b4a.alloc(24, 0x3), b4a.alloc(12, 0x63)] 78 | 79 | sodium.crypto_generichash(hash1, b4a.concat(input)) 80 | sodium.crypto_generichash_batch(hash2, input) 81 | crypto.hash(input, hash3) 82 | 83 | t.alike(hash2, hash1) 84 | t.alike(hash3, hash1) 85 | t.alike(crypto.hash(input), hash1) 86 | t.alike(crypto.hash(b4a.concat(input)), hash1) 87 | }) 88 | 89 | test('namespace', function (t) { 90 | const ns = crypto.namespace('hyperswarm/secret-stream', 2) 91 | 92 | t.alike(ns[0], b4a.from('a931a0155b5c09e6d28628236af83c4b8a6af9af60986edeede9dc5d63192bf7', 'hex')) 93 | t.alike(ns[1], b4a.from('742c9d833d430af4c48a8705e91631eecf295442bbca18996e597097723b1061', 'hex')) 94 | t.is(ns[0].buffer.byteLength < 1000, true, 'no default slab') 95 | t.is(ns[0].buffer, ns[1].buffer, 'slab shared between entries') 96 | }) 97 | 98 | test('namespace (random access)', function (t) { 99 | const ns = crypto.namespace('hyperswarm/secret-stream', [1, 0]) 100 | 101 | t.alike(ns[0], b4a.from('742c9d833d430af4c48a8705e91631eecf295442bbca18996e597097723b1061', 'hex')) 102 | t.alike(ns[1], b4a.from('a931a0155b5c09e6d28628236af83c4b8a6af9af60986edeede9dc5d63192bf7', 'hex')) 103 | }) 104 | 105 | test('another namespace', function (t) { 106 | const ns = crypto.namespace('foo', [1]) 107 | 108 | t.alike(ns[0], b4a.from('fff5eac99641b1b9dee6cabaaeb5959f4b452f7c83769156566aa44de89c82fb', 'hex')) 109 | }) 110 | 111 | test('random namespace', function (t) { 112 | const s = Math.random().toString() 113 | const ns1 = crypto.namespace(s, 10).slice(1) 114 | const ns2 = crypto.namespace(s, [1, 2, 3, 4, 5, 6, 7, 8, 9]) 115 | 116 | t.alike(ns1, ns2) 117 | }) 118 | 119 | test('discovery key does not use slabs', function (t) { 120 | const key = b4a.allocUnsafe(32) 121 | const discKey = crypto.discoveryKey(key) 122 | t.is(discKey.buffer.byteLength, 32, 'does not use slab memory') 123 | }) 124 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const sodium = require('sodium-universal') 2 | const c = require('compact-encoding') 3 | const b4a = require('b4a') 4 | 5 | // https://en.wikipedia.org/wiki/Merkle_tree#Second_preimage_attack 6 | const LEAF_TYPE = b4a.from([0]) 7 | const PARENT_TYPE = b4a.from([1]) 8 | const ROOT_TYPE = b4a.from([2]) 9 | 10 | const HYPERCORE = b4a.from('hypercore') 11 | 12 | exports.keyPair = function (seed) { 13 | // key pairs might stay around for a while, so better not to use a default slab to avoid retaining it completely 14 | const slab = b4a.allocUnsafeSlow(sodium.crypto_sign_PUBLICKEYBYTES + sodium.crypto_sign_SECRETKEYBYTES) 15 | const publicKey = slab.subarray(0, sodium.crypto_sign_PUBLICKEYBYTES) 16 | const secretKey = slab.subarray(sodium.crypto_sign_PUBLICKEYBYTES) 17 | 18 | if (seed) sodium.crypto_sign_seed_keypair(publicKey, secretKey, seed) 19 | else sodium.crypto_sign_keypair(publicKey, secretKey) 20 | 21 | return { 22 | publicKey, 23 | secretKey 24 | } 25 | } 26 | 27 | exports.validateKeyPair = function (keyPair) { 28 | const pk = b4a.allocUnsafe(sodium.crypto_sign_PUBLICKEYBYTES) 29 | sodium.crypto_sign_ed25519_sk_to_pk(pk, keyPair.secretKey) 30 | return b4a.equals(pk, keyPair.publicKey) 31 | } 32 | 33 | exports.sign = function (message, secretKey) { 34 | // Dedicated slab for the signature, to avoid retaining unneeded mem and for security 35 | const signature = b4a.allocUnsafeSlow(sodium.crypto_sign_BYTES) 36 | sodium.crypto_sign_detached(signature, message, secretKey) 37 | return signature 38 | } 39 | 40 | exports.verify = function (message, signature, publicKey) { 41 | if (signature.byteLength !== sodium.crypto_sign_BYTES) return false 42 | if (publicKey.byteLength !== sodium.crypto_sign_PUBLICKEYBYTES) return false 43 | return sodium.crypto_sign_verify_detached(signature, message, publicKey) 44 | } 45 | 46 | exports.encrypt = function (message, publicKey) { 47 | const ciphertext = b4a.alloc(message.byteLength + sodium.crypto_box_SEALBYTES) 48 | sodium.crypto_box_seal(ciphertext, message, publicKey) 49 | return ciphertext 50 | } 51 | 52 | exports.decrypt = function (ciphertext, keyPair) { 53 | if (ciphertext.byteLength < sodium.crypto_box_SEALBYTES) return null 54 | 55 | const plaintext = b4a.alloc(ciphertext.byteLength - sodium.crypto_box_SEALBYTES) 56 | 57 | if (!sodium.crypto_box_seal_open(plaintext, ciphertext, keyPair.publicKey, keyPair.secretKey)) { 58 | return null 59 | } 60 | 61 | return plaintext 62 | } 63 | 64 | exports.encryptionKeyPair = function (seed) { 65 | const publicKey = b4a.alloc(sodium.crypto_box_PUBLICKEYBYTES) 66 | const secretKey = b4a.alloc(sodium.crypto_box_SECRETKEYBYTES) 67 | 68 | if (seed) { 69 | sodium.crypto_box_seed_keypair(publicKey, secretKey, seed) 70 | } else { 71 | sodium.crypto_box_keypair(publicKey, secretKey) 72 | } 73 | 74 | return { 75 | publicKey, 76 | secretKey 77 | } 78 | } 79 | 80 | exports.data = function (data) { 81 | const out = b4a.allocUnsafe(32) 82 | 83 | sodium.crypto_generichash_batch(out, [ 84 | LEAF_TYPE, 85 | c.encode(c.uint64, data.byteLength), 86 | data 87 | ]) 88 | 89 | return out 90 | } 91 | 92 | exports.parent = function (a, b) { 93 | if (a.index > b.index) { 94 | const tmp = a 95 | a = b 96 | b = tmp 97 | } 98 | 99 | const out = b4a.allocUnsafe(32) 100 | 101 | sodium.crypto_generichash_batch(out, [ 102 | PARENT_TYPE, 103 | c.encode(c.uint64, a.size + b.size), 104 | a.hash, 105 | b.hash 106 | ]) 107 | 108 | return out 109 | } 110 | 111 | exports.tree = function (roots, out) { 112 | const buffers = new Array(3 * roots.length + 1) 113 | let j = 0 114 | 115 | buffers[j++] = ROOT_TYPE 116 | 117 | for (let i = 0; i < roots.length; i++) { 118 | const r = roots[i] 119 | buffers[j++] = r.hash 120 | buffers[j++] = c.encode(c.uint64, r.index) 121 | buffers[j++] = c.encode(c.uint64, r.size) 122 | } 123 | 124 | if (!out) out = b4a.allocUnsafe(32) 125 | sodium.crypto_generichash_batch(out, buffers) 126 | return out 127 | } 128 | 129 | exports.hash = function (data, out) { 130 | if (!out) out = b4a.allocUnsafe(32) 131 | if (!Array.isArray(data)) data = [data] 132 | 133 | sodium.crypto_generichash_batch(out, data) 134 | 135 | return out 136 | } 137 | 138 | exports.randomBytes = function (n) { 139 | const buf = b4a.allocUnsafe(n) 140 | sodium.randombytes_buf(buf) 141 | return buf 142 | } 143 | 144 | exports.discoveryKey = function (key) { 145 | if (!key || key.byteLength !== 32) throw new Error('Must pass a 32 byte buffer') 146 | // Discovery keys might stay around for a while, so better not to use slab memory (for better gc) 147 | const digest = b4a.allocUnsafeSlow(32) 148 | sodium.crypto_generichash(digest, HYPERCORE, key) 149 | return digest 150 | } 151 | 152 | if (sodium.sodium_free) { 153 | exports.free = function (secureBuf) { 154 | if (secureBuf.secure) sodium.sodium_free(secureBuf) 155 | } 156 | } else { 157 | exports.free = function () {} 158 | } 159 | 160 | exports.namespace = function (name, count) { 161 | const ids = typeof count === 'number' ? range(count) : count 162 | 163 | // Namespaces are long-lived, so better to use a dedicated slab 164 | const buf = b4a.allocUnsafeSlow(32 * ids.length) 165 | 166 | const list = new Array(ids.length) 167 | 168 | // ns is ephemeral, so default slab 169 | const ns = b4a.allocUnsafe(33) 170 | sodium.crypto_generichash(ns.subarray(0, 32), typeof name === 'string' ? b4a.from(name) : name) 171 | 172 | for (let i = 0; i < list.length; i++) { 173 | list[i] = buf.subarray(32 * i, 32 * i + 32) 174 | ns[32] = ids[i] 175 | sodium.crypto_generichash(list[i], ns) 176 | } 177 | 178 | return list 179 | } 180 | 181 | function range (count) { 182 | const arr = new Array(count) 183 | for (let i = 0; i < count; i++) arr[i] = i 184 | return arr 185 | } 186 | --------------------------------------------------------------------------------