├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── LICENSE ├── README.md ├── crypto.js ├── errors.js ├── index.js ├── package.json ├── protocol.js ├── random.js └── test ├── net.js ├── net1.js ├── net2.js ├── secret-handshake.js ├── shs1-test ├── client.js └── server.js └── vectors.js /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 13 | 14 | strategy: 15 | matrix: 16 | node-version: [12.x, 14.x, 16.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm install 25 | - run: npm test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dominic Tarr 4 | 5 | Permission is hereby granted, free of charge, 6 | to any person obtaining a copy of this software and 7 | associated documentation files (the "Software"), to 8 | deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, 10 | merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom 12 | the Software is furnished to do so, 13 | subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice 16 | shall be included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 22 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # secret-handshake 2 | 3 | secure-channel based on a a mutually authenticating key agreement handshake, with forward secure identity metadata. 4 | 5 | For a full explanation of the design, read the 6 | [Design Paper](http://dominictarr.github.io/secret-handshake-paper/shs.pdf) 7 | 8 | ## Implementations 9 | 10 | * javascript/node.js this repo. 11 | * go [cryptix/secretstream](https://github.com/cryptix/secretstream/) 12 | * rust [AljoschaMeyer/secret-handshake-rs](https://github.com/AljoschaMeyer/secret-handshake-rs) 13 | * c [AljoschaMeyer/shs1-c](https://github.com/AljoschaMeyer/shs1-c) (actually just implements the crypto, not the protocol used as a component in the rust implementation) 14 | * python/twisted [david415/txsecrethandshake](https://github.com/david415/txsecrethandshake) (WIP) 15 | * C++ [Kodest/cppshs](https://github.com/Kodest/cppshs) (WIP) 16 | * also [keks/tamarin-shs](https://github.com/keks/tamarin-shs) is a formal proof of the cryptographic properties! 17 | 18 | ## Claims 19 | 20 | This protocol derives shared keys and mutually 21 | authenticates both ends of the connection. 22 | The shared secrets are forward secure, and 23 | so is the identity metadata. 24 | 25 | by "forward secure identity metadata" I mean: 26 | 27 | * a later key compromise cannot confirm the public keys in the handshake. 28 | 29 | And also: 30 | 31 | * an eavesdropper cannot learn public keys 32 | * replay attacker cannot learn public keys. 33 | * man in the middle cannot learn public keys. 34 | * a "wrong number" cannot learn public keys. 35 | * an unauthenticated client cannot learn server key. 36 | 37 | > note: a wrong number is just an accidental man in the middle. 38 | 39 | By "confirm" I mean check a guess at the public key. 40 | By "learn" I mean that you can _either_ extract the public key, 41 | or confirm the public key. 42 | 43 | Also note that if the server decides not to authenticate a client, 44 | it will learn their public key. To get to this stage, the client 45 | must know the server's key, so now the client and server both 46 | know each others key. This is fair. 47 | 48 | ## Disclaims 49 | 50 | This protocol cannot hide your ip address. 51 | This protocol does not attempt to obscure packet boundries. 52 | If a man in the middle or wrong number later compromises 53 | the server's key, they will be able to extract the client 54 | key from the client's hello packet. 55 | 56 | ## Example 57 | 58 | ``` js 59 | var SHS = require('secret-handshake') 60 | 61 | var cl = require('chloride') 62 | var appKey = ... //32 random bytes 63 | var alice = cl.crypto_sign_keypair() //client 64 | var bob = cl.crypto_sign_keypair() //server 65 | 66 | function authorize(id, cb) { 67 | cb(null, check(id)) //check wether id is authorized. 68 | } 69 | 70 | //initialize, with default timeouts. 71 | var ServerStream = SHS.createServer(alice, authorize, appKey) 72 | var ClientStream = SHS.createClient(bob, appkey) 73 | 74 | var alice_stream = ServerStream(function (err, stream) { 75 | ... 76 | }) 77 | 78 | var bob_stream = ClientStream(alice.publicKey, function (err, stream) { 79 | ... 80 | }) 81 | 82 | //simulate a streaming network connection by connecting streams together 83 | pull(alice_stream, bob_stream, alice_stream) 84 | ``` 85 | 86 | ## Notes 87 | 88 | I recommend using secret-handshake via [multiserver](https://github.com/dominictarr/multiserver) 89 | 90 | [pull-streams](https://github.com/dominictarr/pull-streams) are used. 91 | Learn about how pull-streams from [these examples](https://github.com/dominictarr/pull-stream-examples) 92 | 93 | Keypairs are expected to be of the form [sodium](https://github.com/paixaop/node-sodium) produces. 94 | [chloride](https://github.com/dominictarr/chloride) is my fork of this and is compatible. 95 | 96 | If you're interested in the protocol, you can read more here : https://ssbc.github.io/scuttlebutt-protocol-guide/#handshake 97 | 98 | ## api 99 | 100 | ### createClient(keypair, authorize, appkey, timeout) => createClientStream(key, seed?, cb(err, plainstream)) => cipherstream 101 | 102 | `createClient` takes: 103 | - `keypair` - a keypair of form `{ secretKey, publicKey }` - your clients keys (see `chloride#crypto_sign_keypair`) 104 | - `appkey` - the network identifier, 32 random bytes 105 | - `timeout` - an integer (in milliseconds? CHECK THIS) 106 | 107 | and returns a `createClientStream` 108 | 109 | `createClientStream` takes a the public `key` for the remote peer, 110 | an optional `seed` (which is used to generate a one-time private key), 111 | and a callback, `cb`. `cipherstream`, an encrypted duplex pull-stream is returned. 112 | 113 | Once the stream is connected to a server stream, 114 | secret-handshake will attempt to authorize, and will call 115 | `cb` with an `err` if it fails, or `plainstream` if it succeeds. 116 | If `keypair` is null, `seed` *must* be provided. 117 | 118 | ### createServer(keypair, authorize(id, cb), appkey, timeout) => createServerStream(cb(err, plain_stream)) => cipherstream 119 | 120 | `createServer` is similar, except it takes `authorize`, 121 | - `keypair` - a keypair of form `{ secretKey, publicKey }` (see `chloride#crypto_sign_keypair`) 122 | - `authorize` - an async function of signature `(id, cb)` that decides whether a client with id == publicKey is allowed to continue with handshake 123 | - `appkey` - the network identifier, 32 random bytes 124 | - `timeout` - an integer (in milliseconds? CHECK THIS) 125 | 126 | A stream constructor function is returned 127 | Note the server DOES NOT take the client id as an argument - instead, in the process 128 | of the handshake, the server learns the `id`, and passes it to `authorize`. 129 | If `authorize` calls back truthy, then it will callback `cb(null, plainstream)` 130 | else it errors, `cb(err)`. 131 | The value that `authorize` calls back `cb(null, )` will be assigned to `plainstream.auth = `. 132 | Also, the `id` of the remote will be assigned to `plainstream.id`. 133 | This way the application layer can know who it's peer is. 134 | 135 | ## License 136 | 137 | MIT 138 | -------------------------------------------------------------------------------- /crypto.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var sodium = require('chloride') 3 | 4 | // var keypair = sodium.crypto_box_seed_keypair 5 | var from_seed = sodium.crypto_sign_seed_keypair 6 | var shared = sodium.crypto_scalarmult 7 | var hash = sodium.crypto_hash_sha256 8 | var sign = sodium.crypto_sign_detached 9 | var verify = sodium.crypto_sign_verify_detached 10 | var auth = sodium.crypto_auth 11 | var verify_auth = sodium.crypto_auth_verify 12 | var curvify_pk = sodium.crypto_sign_ed25519_pk_to_curve25519 13 | var curvify_sk = sodium.crypto_sign_ed25519_sk_to_curve25519 14 | var box = sodium.crypto_secretbox_easy 15 | var unbox = sodium.crypto_secretbox_open_easy 16 | 17 | var concat = Buffer.concat 18 | 19 | var nonce = Buffer.alloc(24); nonce.fill(0) 20 | 21 | var isBuffer = Buffer.isBuffer 22 | 23 | exports.challenge_length = 64 24 | exports.client_auth_length = 16+32+64 25 | exports.server_auth_length = 16+64 26 | exports.mac_length = 16 27 | 28 | //both client and server 29 | 30 | function assert_length(buf, name, length) { 31 | if(buf.length !== length) 32 | throw new Error('expected '+name+' to have length' + length + ', but was:'+buf.length) 33 | } 34 | 35 | exports.initialize = function (state) { 36 | 37 | if(state.seed) state.local = from_seed(state.seed) 38 | 39 | //TODO: sodium is missing box_seed_keypair. should make PR for that. 40 | // mix: sodium-native has this fn https://github.com/sodium-friends/sodium-native 41 | 42 | var _key = from_seed(state.random) 43 | // var kx = keypair(random) 44 | var kx_pk = curvify_pk(_key.publicKey) 45 | var kx_sk = curvify_sk(_key.secretKey) 46 | 47 | state.local = { 48 | kx_pk: kx_pk, 49 | kx_sk: kx_sk, 50 | publicKey: state.local.publicKey, 51 | secretKey: state.local.secretKey, 52 | app_mac: auth(kx_pk, state.app_key) 53 | } 54 | 55 | state.remote = state.remote || {} 56 | 57 | return state 58 | } 59 | 60 | exports.createChallenge = function (state) { 61 | return concat([state.local.app_mac, state.local.kx_pk]) 62 | } 63 | 64 | 65 | exports.verifyChallenge = function (state, challenge) { 66 | assert_length(challenge, 'challenge', exports.challenge_length) 67 | 68 | var mac = challenge.slice(0, 32) 69 | var remote_pk = challenge.slice(32, exports.challenge_length) 70 | 71 | if(0 !== verify_auth(mac, remote_pk, state.app_key)) 72 | return null 73 | 74 | state.remote.kx_pk = remote_pk 75 | state.remote.app_mac = mac 76 | state.secret = shared(state.local.kx_sk, state.remote.kx_pk) 77 | state.shash = hash(state.secret) 78 | 79 | return state 80 | } 81 | 82 | exports.clean = function (state) { 83 | // clean away all the secrets for forward security. 84 | // use a different secret hash(secret3) in the rest of the session, 85 | // and so that a sloppy application cannot compromise the handshake. 86 | 87 | state.shash.fill(0) 88 | state.secret.fill(0) 89 | state.a_bob.fill(0) 90 | state.b_alice.fill(0) 91 | 92 | state.secret = hash(state.secret3) 93 | state.encryptKey = hash(concat([state.secret, state.remote.publicKey])) 94 | state.decryptKey = hash(concat([state.secret, state.local.publicKey])) 95 | 96 | state.secret2.fill(0) 97 | state.secret3.fill(0) 98 | state.local.kx_sk.fill(0) 99 | 100 | state.shash = null 101 | state.secret2 = null 102 | state.secret3 = null 103 | state.a_bob = null 104 | state.b_alice = null 105 | state.local.kx_sk = null 106 | return state 107 | } 108 | 109 | //client side only (Alice) 110 | 111 | exports.clientVerifyChallenge = function (state, challenge) { 112 | assert_length(challenge, 'challenge', exports.challenge_length) 113 | state = exports.verifyChallenge(state, challenge) 114 | if(!state) return null 115 | 116 | //now we have agreed on the secret. 117 | //this can be an encryption secret, 118 | //or a hmac secret. 119 | var curve = curvify_pk(state.remote.publicKey) 120 | if(!curve) return null 121 | var a_bob = shared(state.local.kx_sk, curve) 122 | state.a_bob = a_bob 123 | state.secret2 = hash(concat([state.app_key, state.secret, a_bob])) 124 | 125 | var signed = concat([state.app_key, state.remote.publicKey, state.shash]) 126 | var sig = sign(signed, state.local.secretKey) 127 | 128 | state.local.hello = Buffer.concat([sig, state.local.publicKey]) 129 | return state 130 | } 131 | 132 | exports.clientCreateAuth = function (state) { 133 | return box(state.local.hello, nonce, state.secret2) 134 | } 135 | 136 | exports.clientVerifyAccept = function (state, boxed_okay) { 137 | assert_length(boxed_okay, 'server_auth', exports.server_auth_length) 138 | 139 | var b_alice = shared(curvify_sk(state.local.secretKey), state.remote.kx_pk) 140 | state.b_alice = b_alice 141 | state.secret3 = hash(concat([state.app_key, state.secret, state.a_bob, state.b_alice])) 142 | 143 | var sig = unbox(boxed_okay, nonce, state.secret3) 144 | if(!sig) return null 145 | var signed = concat([state.app_key, state.local.hello, state.shash]) 146 | if(!verify(sig, signed, state.remote.publicKey)) 147 | return null 148 | return state 149 | } 150 | 151 | //server side only (Bob) 152 | 153 | exports.serverVerifyAuth = function (state, data) { 154 | assert_length(data, 'client_auth', exports.client_auth_length) 155 | 156 | var a_bob = shared(curvify_sk(state.local.secretKey), state.remote.kx_pk) 157 | state.a_bob = a_bob 158 | state.secret2 = hash(concat([state.app_key, state.secret, a_bob])) 159 | 160 | state.remote.hello = unbox(data, nonce, state.secret2) 161 | if(!state.remote.hello) 162 | return null 163 | 164 | var sig = state.remote.hello.slice(0, 64) 165 | var publicKey = state.remote.hello.slice(64, 96) 166 | 167 | var signed = concat([state.app_key, state.local.publicKey, state.shash]) 168 | if(!verify(sig, signed, publicKey)) 169 | return null 170 | 171 | state.remote.publicKey = publicKey 172 | //shared key between my local ephemeral key + remote public 173 | var b_alice = shared(state.local.kx_sk, curvify_pk(state.remote.publicKey)) 174 | state.b_alice = b_alice 175 | state.secret3 = hash(concat([state.app_key, state.secret, state.a_bob, state.b_alice])) 176 | 177 | return state 178 | 179 | } 180 | 181 | exports.serverCreateAccept = function (state) { 182 | var signed = concat([state.app_key, state.remote.hello, state.shash]) 183 | var okay = sign(signed, state.local.secretKey) 184 | return box(okay, nonce, state.secret3) 185 | } 186 | 187 | exports.toKeys = function (keys) { 188 | if(isBuffer(keys, 32)) 189 | return sodium.crypto_sign_seed_keypair(keys) 190 | return keys 191 | } 192 | -------------------------------------------------------------------------------- /errors.js: -------------------------------------------------------------------------------- 1 | //phases: 2 | // 1 client sends challenge 3 | // 2 server sends challenge 4 | // 3 client sends hello (include proof they know the server) 5 | // 4 server decides if they want client to connect with them 6 | // 5 server sends acknowledgement to client 7 | 8 | module.exports = { 9 | serverErrorOnChallenge: 10 | "shs.client: error when expecting server to accept challenge (phase 1).\n" + 11 | "possibly the server is busy, does not speak shs or uses a different application cap", 12 | 13 | serverInvalidChallenge: 14 | "shs.client: server responded with invalid challenge (phase 2). possibly server does not speak shs", 15 | 16 | serverHungUp: 17 | "shs.client: server hung up when we sent hello (phase 3).\n" + 18 | "Possibly we dailed a wrong number, or the server does not wish to talk to us.", 19 | 20 | serverAcceptInvalid: 21 | "shs.client: the server's response accepting us our hello (phase 5) was invalid, so we hung up", 22 | 23 | clientErrorOnChallenge: 24 | "shs.server: error when waiting for client to send challenge (phase 1)", 25 | 26 | clientInvalidChallenge: 27 | "shs.server: client sent invalid challenge (phase 1), possibly they tried to speak a different protocol or had wrong application cap", 28 | 29 | //we got a networking error: 30 | clientErrorOnHello: 31 | "shs.server: error when expecting client to say hello (phase 2)", 32 | 33 | clientInvalidHello: 34 | "shs.server: client hello invalid (phase 3). they dailed a wrong number - they didn't have our public key", 35 | 36 | clientUnauthorized: 37 | "shs.server: we did not authorize the client (phase 4), so we hung up.", 38 | 39 | serverErrorOnAuthorization: 40 | "shs.server: while trying to decide if the client should be authorized (phase 4), we got an error on the server. This could be a database error", 41 | } 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = require('./protocol')(require('./crypto')) 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secret-handshake", 3 | "description": "a simple and highly private secure-channel protocol", 4 | "version": "1.1.21", 5 | "homepage": "https://github.com/auditdrivencrypto/secret-handshake", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/auditdrivencrypto/secret-handshake.git" 9 | }, 10 | "files": [ 11 | "*.js" 12 | ], 13 | "scripts": { 14 | "prepublishOnly": "npm ls && npm test", 15 | "test": "npm-run-all test:original test:shs1-test", 16 | "test:original": "set -e; for t in test/*.js; do node $t; done", 17 | "test:shs1-test": "npm-run-all test:shs1-test:*", 18 | "test:shs1-test:server": "shs1testserver test/shs1-test/server.js", 19 | "test:shs1-test:client": "shs1testclient test/shs1-test/client.js" 20 | }, 21 | "dependencies": { 22 | "chloride": "^2.2.8", 23 | "clarify-error": "^1.0.0", 24 | "pull-box-stream": "^1.0.13", 25 | "pull-handshake": "^1.1.1", 26 | "pull-stream": "^3.4.5" 27 | }, 28 | "devDependencies": { 29 | "npm-run-all": "^4.1.5", 30 | "pull-bitflipper": "~0.1.0", 31 | "pull-defer": "^0.2.2", 32 | "pull-hang": "0.0.0", 33 | "shs1-test": "^1.1.0", 34 | "stream-to-pull-stream": "^1.7.3", 35 | "tape": "^4.10.1", 36 | "test-secret-handshake": "^1.0.0" 37 | }, 38 | "author": "Dominic Tarr (http://dominictarr.com)", 39 | "license": "MIT" 40 | } 41 | -------------------------------------------------------------------------------- /protocol.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var pull = require('pull-stream') 3 | var boxes = require('pull-box-stream') 4 | var clarify = require('clarify-error') 5 | var errors = require('./errors') 6 | var Handshake = require('pull-handshake') 7 | var random = require('./random') 8 | 9 | function isBuffer(buf, len) { 10 | return Buffer.isBuffer(buf) && buf.length === len 11 | } 12 | 13 | module.exports = function (stateless) { 14 | var exports = {} 15 | //client is Alice 16 | //create the client stream with the public key you expect to connect to. 17 | exports.createClientStream = function (alice, app_key, timeout) { 18 | 19 | return function (bob_pub, seed, cb) { 20 | if('function' == typeof seed) 21 | cb = seed, seed = null 22 | 23 | //alice may be null, e.g. https://github.com/ssbc/ssb-invite/blob/b93918b3e6adcb8dd68674fdbb270b49ff07f2a8/index.js#L219 24 | var state = stateless.initialize({ 25 | app_key: app_key, 26 | local: alice, 27 | remote: {publicKey: bob_pub}, 28 | seed: seed, 29 | random: random(32) 30 | }) 31 | 32 | var stream = Handshake({timeout: timeout}, cb) 33 | var shake = stream.handshake 34 | stream.handshake = null 35 | 36 | function abort(err, reason) { 37 | if(err && err !== true) shake.abort(clarify(err, reason), cb) 38 | else shake.abort(new Error(reason), cb) 39 | } 40 | 41 | shake.write(stateless.createChallenge(state)) 42 | 43 | shake.read(stateless.challenge_length, function (err, msg) { 44 | if(err) return abort(err, errors.serverErrorOnChallenge) 45 | //create the challenge first, because we need to generate a local key 46 | if(!(state = stateless.clientVerifyChallenge(state, msg))) 47 | return abort(null, errors.serverInvalidChallenge) 48 | 49 | shake.write(stateless.clientCreateAuth(state)) 50 | 51 | shake.read(stateless.server_auth_length, function (err, boxed_sig) { 52 | if(err) return abort(err, errors.serverHungUp) 53 | 54 | if(!(state = stateless.clientVerifyAccept(state, boxed_sig))) 55 | return abort(null, errors.serverAcceptInvalid) 56 | 57 | cb(null, shake.rest(), state = stateless.clean(state)) 58 | }) 59 | }) 60 | 61 | return stream 62 | } 63 | } 64 | 65 | //server is Bob. 66 | exports.createServerStream = function (bob, authorize, app_key, timeout) { 67 | 68 | return function (cb) { 69 | var state = stateless.initialize({ 70 | app_key: app_key, 71 | local: bob, 72 | //note, the server doesn't know the remote until it receives ClientAuth 73 | random: random(32) 74 | }) 75 | var stream = Handshake({timeout: timeout}, cb) 76 | 77 | var shake = stream.handshake 78 | stream.handshake = null 79 | 80 | function abort (err, reason) { 81 | if(err && err !== true) shake.abort(err) 82 | else shake.abort(new Error(reason)) 83 | // shake.abort(err) triggers cb(err) 84 | } 85 | 86 | shake.read(stateless.challenge_length, function (err, challenge) { 87 | if(err) return abort(err, errors.clientErrorOnChallenge) 88 | if(!(state = stateless.verifyChallenge(state, challenge))) 89 | return shake.abort(new Error(errors.clientInvalidChallenge)) 90 | 91 | shake.write(stateless.createChallenge(state)) 92 | shake.read(stateless.client_auth_length, function (err, hello) { 93 | if(err) return abort(err, errors.clientErrorOnHello) 94 | 95 | if(!(state = stateless.serverVerifyAuth(state, hello))) 96 | return abort(null, errors.clientInvalidHello) 97 | 98 | //check if the user wants to speak to alice. 99 | authorize(state.remote.publicKey, function (err, auth) { 100 | if(err) return abort(err, errors.serverErrorOnAuthorization) 101 | if(!auth) return abort(null, errors.clientUnauthorized) 102 | state.auth = auth 103 | shake.write(stateless.serverCreateAccept(state)) 104 | cb(null, shake.rest(), state = stateless.clean(state)) 105 | }) 106 | }) 107 | }) 108 | return stream 109 | } 110 | } 111 | 112 | //wrap the above into an actual handshake + encrypted session 113 | 114 | exports.toKeys = stateless.toKeys 115 | 116 | function secure (cb) { 117 | return function (err, stream, state) { 118 | if(err) return cb(err) 119 | 120 | var encryptNonce = state.remote.app_mac.slice(0, 24) 121 | var decryptNonce = state.local.app_mac.slice(0, 24) 122 | 123 | cb(null, { 124 | remote: state.remote.publicKey, 125 | //on the server, attach any metadata gathered 126 | //during `authorize` call 127 | auth: state.auth, 128 | crypto: { 129 | encryptKey: state.encryptKey, 130 | decryptKey: state.decryptKey, 131 | encryptNonce: encryptNonce, 132 | decryptNonce: decryptNonce 133 | }, 134 | source: pull( 135 | stream.source, 136 | boxes.createUnboxStream(state.decryptKey, decryptNonce) 137 | ), 138 | sink: pull( 139 | boxes.createBoxStream(state.encryptKey, encryptNonce), 140 | stream.sink 141 | ) 142 | }) 143 | } 144 | } 145 | 146 | exports.client = 147 | exports.createClient = function (alice, app_key, timeout) { 148 | var create = exports.createClientStream(alice, app_key, timeout) 149 | 150 | return function (bob, seed, cb) { 151 | if(!isBuffer(bob, 32)) 152 | throw new Error('createClient *must* be passed a public key') 153 | if('function' === typeof seed) 154 | return create(bob, secure(seed)) 155 | else 156 | return create(bob, seed, secure(cb)) 157 | } 158 | } 159 | 160 | exports.server = 161 | exports.createServer = function (bob, authorize, app_key, timeout) { 162 | var create = exports.createServerStream(bob, authorize, app_key, timeout) 163 | 164 | return function (cb) { 165 | return create(secure(cb)) 166 | } 167 | } 168 | 169 | return exports 170 | } 171 | -------------------------------------------------------------------------------- /random.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var cl = require('chloride') 3 | 4 | module.exports = function (bytes) { 5 | var b = Buffer.alloc(bytes) 6 | cl.randombytes(b, bytes) 7 | return b 8 | } 9 | -------------------------------------------------------------------------------- /test/net.js: -------------------------------------------------------------------------------- 1 | // will probably remove this. 2 | // I am now moving using secret-handshake via 3 | // https://github.com/dominictarr/multiserver 4 | // instead. 5 | 6 | var sodium = require('chloride') 7 | var net = require('net') 8 | var toPull = require('stream-to-pull-stream') 9 | var shs = require('../') 10 | var isBuffer = Buffer.isBuffer 11 | var pull = require('pull-stream') 12 | var Defer = require('pull-defer/duplex') 13 | 14 | function assertOpts (opts) { 15 | if(!(opts && 'object' === typeof opts)) 16 | throw new Error('opts *must* be provided') 17 | } 18 | function assertKeys (opts) { 19 | if(!( 20 | opts.keys 21 | && isBuffer(opts.keys.publicKey) 22 | && isBuffer(opts.keys.secretKey) 23 | )) 24 | throw new Error('opts.keys = ed25519 key pair *must* be provided.') 25 | } 26 | 27 | function assertAppKey (opts) { 28 | if(!isBuffer(opts.appKey)) 29 | throw new Error('appKey must be provided') 30 | } 31 | 32 | function assertAddr (addr) { 33 | if(!isBuffer(addr.key)) 34 | throw new Error('opts.key *must* be an ed25519 public key') 35 | if(!Number.isInteger(+addr.port)) 36 | throw new Error('opts.port *must* be provided') 37 | if(!('string' === typeof addr.host || null == addr.host)) 38 | throw new Error('opts.host must be string or null') 39 | } 40 | 41 | module.exports = function createNode (opts) { 42 | var keys = 43 | isBuffer(opts.seed) 44 | ? sodium.crypto_sign_seed_keypair(opts.seed) 45 | : opts.keys 46 | 47 | assertOpts(opts); assertKeys({keys: keys}); assertAppKey(opts) 48 | 49 | var create = shs.createClient(keys, opts.appKey, opts.timeout) 50 | 51 | return { 52 | publicKey: keys.publicKey, 53 | createServer: function (onConnect) { 54 | if('function' !== typeof opts.authenticate) 55 | throw new Error('function opts.authenticate(pub, cb)' 56 | + '*must* be provided in order to receive connections') 57 | var createServerStream = 58 | shs.createServer(keys, opts.authenticate, opts.appKey, opts.timeout) 59 | var server 60 | return server = net.createServer(function (stream) { 61 | stream = toPull.duplex(stream) 62 | pull( 63 | stream, 64 | createServerStream(function (err, stream) { 65 | if(err) return server.emit('unauthenticated', err) 66 | onConnect(stream) 67 | }), 68 | stream 69 | ) 70 | }) 71 | }, 72 | connect: function (addr, cb) { 73 | assertAddr(addr) 74 | var stream = toPull.duplex(net.connect(addr.port, addr.host)) 75 | 76 | if(cb) { 77 | pull( 78 | stream, 79 | create(addr.key, cb), 80 | stream 81 | ) 82 | } 83 | else { 84 | 85 | var defer = Defer() 86 | 87 | pull( 88 | stream, 89 | create(addr.key, function (err, stream) { 90 | if(err) 91 | defer.resolve({ 92 | source: function (abort, cb) { cb(err) }, 93 | sink: function (read) { read(err, function (){}) } 94 | }) 95 | else defer.resolve(stream) 96 | }), 97 | stream 98 | ) 99 | 100 | return defer 101 | 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /test/net1.js: -------------------------------------------------------------------------------- 1 | 2 | var net = require('net') 3 | var pull = require('pull-stream') 4 | var toPull = require('stream-to-pull-stream') 5 | var tape = require('tape') 6 | 7 | var cl = require('chloride') 8 | function hash (str) { 9 | return cl.crypto_hash_sha256(Buffer.from(str)) 10 | } 11 | 12 | var alice = cl.crypto_sign_seed_keypair(hash('alice')) 13 | var bob = cl.crypto_sign_seed_keypair(hash('bob')) 14 | 15 | var app_key = hash('app_key') 16 | 17 | var shs = require('../') 18 | 19 | tape('test with net', function (t) { 20 | 21 | var createServer = shs.createServer(bob, function (pub, cb) { 22 | cb(null, true) //accept 23 | }, app_key, 100) 24 | 25 | var createClient = shs.createClient(alice, app_key, 100) 26 | 27 | 28 | var PORT = 45034 29 | 30 | var server = net.createServer(function (stream) { 31 | 32 | stream = toPull.duplex(stream) 33 | 34 | pull( 35 | stream, 36 | createServer(function (err, stream) { 37 | console.log('server connected', err, stream) 38 | 39 | pull(stream, stream) //echo 40 | }), 41 | stream 42 | ) 43 | 44 | }).listen(PORT, function () { 45 | 46 | var stream = toPull.duplex(net.connect(PORT)) 47 | 48 | console.log('CLIENT connect') 49 | pull( 50 | stream, 51 | createClient(bob.publicKey, function (err, stream) { 52 | console.log('client connected', err, stream) 53 | pull( 54 | pull.values([Buffer.from('HELLO')]), 55 | stream, 56 | pull.collect(function (err, data) { 57 | t.notOk(err) 58 | t.deepEqual(Buffer.concat(data), Buffer.from('HELLO')) 59 | server.close() 60 | t.end() 61 | }) 62 | ) 63 | }), 64 | stream 65 | ) 66 | 67 | }) 68 | 69 | }) 70 | 71 | tape('test with net', function (t) { 72 | var n = 2 73 | t.plan(2) 74 | var createServer = shs.createServer(bob, function (pub, cb) { 75 | cb() //reject, with no reason 76 | }, app_key, 100) 77 | 78 | var createClient = shs.createClient(alice, app_key, 100) 79 | 80 | var PORT = 45035 81 | 82 | var server = net.createServer(function (stream) { 83 | 84 | stream = toPull.duplex(stream) 85 | 86 | pull( 87 | stream, 88 | createServer(function (err, stream) { 89 | t.ok(err) 90 | console.log('server connected', err, stream) 91 | next() 92 | }), 93 | stream 94 | ) 95 | 96 | }).listen(PORT, function () { 97 | 98 | var stream = toPull.duplex(net.connect(PORT)) 99 | 100 | console.log('CLIENT connect') 101 | pull( 102 | stream, 103 | createClient(bob.publicKey, function (err, stream) { 104 | console.log('client connected', err, stream) 105 | t.ok(err) 106 | next() 107 | }), 108 | stream 109 | ) 110 | 111 | }) 112 | 113 | function next() { 114 | if(--n) return 115 | server.close() 116 | } 117 | 118 | }) 119 | -------------------------------------------------------------------------------- /test/net2.js: -------------------------------------------------------------------------------- 1 | 2 | var netshs = require('./net') 3 | 4 | 5 | var pull = require('pull-stream') 6 | var tape = require('tape') 7 | 8 | var cl = require('chloride') 9 | 10 | function hash (str) { 11 | return cl.crypto_hash_sha256(Buffer.from(str)) 12 | } 13 | 14 | var alice = cl.crypto_sign_seed_keypair(hash('alice')) 15 | var bob = cl.crypto_sign_seed_keypair(hash('bob')) 16 | 17 | var crypto = require('crypto') 18 | var app_key = crypto.randomBytes(32) 19 | 20 | var shs = require('../') 21 | 22 | var bobN = netshs({ 23 | keys: bob, 24 | appKey: app_key, 25 | authenticate: function (pub, cb) { 26 | cb(null, true) //accept 27 | }, 28 | timeout: 200 29 | }) 30 | 31 | var aliceN = netshs({ 32 | keys: alice, 33 | appKey: app_key, 34 | timeout: 200 35 | //alice doesn't need authenticate 36 | //because she is the client. 37 | }) 38 | var PORT = 45034 39 | 40 | tape('test net.js, correct, callback', function (t) { 41 | 42 | 43 | var server = bobN.createServer(function (stream) { 44 | t.deepEqual(stream.remote, alice.publicKey) 45 | pull(stream, pull.through(console.log), stream) //echo 46 | }).listen(PORT, function () { 47 | aliceN.connect( 48 | {host: 'localhost', port: PORT, key: bob.publicKey}, 49 | function (err, stream) { 50 | if(err) throw err 51 | t.deepEqual(stream.remote, bob.publicKey) 52 | pull( 53 | pull.values([Buffer.from('HELLO')]), 54 | stream, 55 | pull.collect(function (err, data) { 56 | if(err) throw err 57 | t.notOk(err) 58 | t.deepEqual(Buffer.concat(data), Buffer.from('HELLO')) 59 | server.close() 60 | t.end() 61 | }) 62 | ) 63 | } 64 | ) 65 | }) 66 | 67 | }) 68 | 69 | tape('test net.js, correct, stream directly', function (t) { 70 | 71 | var server = bobN.createServer(function (stream) { 72 | t.deepEqual(stream.remote, alice.publicKey) 73 | pull(stream, pull.through(console.log), stream) //echo 74 | }).listen(PORT, function () { 75 | pull( 76 | pull.values([Buffer.from('HELLO')]), 77 | aliceN.connect({port: PORT, key: bob.publicKey}), 78 | pull.collect(function (err, data) { 79 | if(err) throw err 80 | t.notOk(err) 81 | t.deepEqual(Buffer.concat(data), Buffer.from('HELLO')) 82 | server.close() 83 | t.end() 84 | }) 85 | ) 86 | }) 87 | 88 | }) 89 | 90 | var bobN2 = netshs({ 91 | keys: bob, 92 | appKey: app_key, 93 | authenticate: function (pub, cb) { 94 | cb() //reject, with no reason 95 | } 96 | }) 97 | 98 | tape('test net, error, callback', function (t) { 99 | 100 | var server = bobN2.createServer(function (stream) { 101 | throw new Error('this should never be called') 102 | }).listen(PORT, function () { 103 | 104 | console.log('CLIENT connect') 105 | aliceN.connect({ 106 | port: PORT, 107 | key: bob.publicKey 108 | }, function (err, stream) { 109 | console.log('client connected', err, stream) 110 | t.ok(err) 111 | t.end() 112 | server.close() 113 | }) 114 | }) 115 | 116 | }) 117 | 118 | 119 | tape('test net, error, stream', function (t) { 120 | 121 | var server = bobN2.createServer(function (stream) { 122 | throw new Error('this should never be called') 123 | }).listen(PORT, function () { 124 | 125 | pull( 126 | aliceN.connect({ 127 | port: PORT, 128 | key: bob.publicKey 129 | }), 130 | pull.collect(function (err, ary) { 131 | t.ok(err) 132 | t.end() 133 | server.close() 134 | }) 135 | ) 136 | }) 137 | 138 | }) 139 | 140 | tape('test net, create seed cap', function (t) { 141 | 142 | var seed = crypto.randomBytes(32) 143 | var keys = cl.crypto_sign_seed_keypair(seed) 144 | 145 | var seedN = netshs({ 146 | seed: seed, 147 | appKey: app_key, 148 | //alice doesn't need authenticate 149 | //because she is the client. 150 | }) 151 | 152 | var server = bobN.createServer(function (stream) { 153 | t.deepEqual(stream.remote, keys.publicKey) 154 | stream.source(true, function () {}) 155 | server.close() 156 | t.end() 157 | }).listen(PORT, function () { 158 | 159 | seedN.connect({port: PORT, key: bob.publicKey}) 160 | 161 | }) 162 | 163 | }) 164 | -------------------------------------------------------------------------------- /test/secret-handshake.js: -------------------------------------------------------------------------------- 1 | 2 | var shs = require('../') 3 | 4 | var tape = require('tape') 5 | var pull = require('pull-stream') 6 | 7 | var cl = require('chloride') 8 | var deepEqual = require('deep-equal') 9 | var bitflipper = require('pull-bitflipper') 10 | var Hang = require('pull-hang') 11 | 12 | function hash (str) { 13 | return cl.crypto_hash_sha256(Buffer.from(str)) 14 | } 15 | 16 | var alice = cl.crypto_sign_seed_keypair(hash('alice')) 17 | var bob = cl.crypto_sign_seed_keypair(hash('bob')) 18 | var wally = cl.crypto_sign_seed_keypair(hash('wally')) 19 | 20 | //var secure = require('../secure') 21 | 22 | var app_key = hash('app_key') 23 | 24 | function unauthorized (_, cb) { 25 | cb(new Error('unauthorized')) 26 | } 27 | 28 | function authorized (_, cb) { 29 | cb() 30 | } 31 | 32 | tape('test handshake', function (t) { 33 | 34 | var aliceHS = shs.client(alice, app_key, 100) 35 | (bob.publicKey, function (err, stream) { 36 | 37 | if(err) throw err 38 | 39 | pull( 40 | pull.values([Buffer.from('hello there')]), 41 | stream, 42 | pull.collect(function (err, hello_there) { 43 | t.equal(hello_there.toString(), 'hello there') 44 | console.log('output:', hello_there.join('')) 45 | t.end() 46 | }) 47 | ) 48 | 49 | }) 50 | 51 | var r = Math.random() 52 | var bobHS = shs.server(bob, function (publicKey, cb) { 53 | t.deepEqual(publicKey, alice.publicKey) 54 | 55 | if(deepEqual(publicKey, alice.publicKey)) cb(null, {okay: true, random: r}) 56 | else 57 | cb(new Error('unauthorized')) 58 | 59 | }, app_key, 100) 60 | (function (err, stream) { 61 | 62 | if(err) throw err 63 | 64 | t.deepEqual(stream.auth, {okay: true, random: r}) 65 | pull(stream, pull.through(function (data) { 66 | console.log('echo:', data.toString()) 67 | }), stream) //ECHO 68 | }) 69 | 70 | pull( 71 | aliceHS, 72 | pull.through(console.log.bind(console, 'A->B')), 73 | bobHS, 74 | pull.through(console.log.bind(console, 'A<-B')), 75 | aliceHS 76 | ) 77 | 78 | }) 79 | 80 | function bitflipTest(t, test) { 81 | var errs = 0 82 | var aliceHS = shs.client(alice, app_key, 100) 83 | (bob.publicKey, function (err) { 84 | t.ok(err, 'Alice errored') 85 | if(++errs === 2) t.end() 86 | }) 87 | 88 | var bobHS = shs.server(bob, function (publicKey, cb) { 89 | t.deepEqual(publicKey, alice.publicKey) 90 | 91 | if(deepEqual(publicKey, alice.publicKey)) cb(null) 92 | else 93 | cb(new Error('unauthorized')) 94 | 95 | }, app_key, 100) 96 | (function (err) { 97 | t.ok(err, 'Bob errored') 98 | if(++errs === 2) t.end() 99 | }) 100 | 101 | test(aliceHS, bobHS) 102 | 103 | } 104 | 105 | tape('test auth fails when first packet is flipped', function (t) { 106 | bitflipTest(t, function (aliceHS, bobHS) { 107 | pull( 108 | aliceHS, 109 | bitflipper(1), 110 | bobHS, 111 | aliceHS 112 | ) 113 | }) 114 | }) 115 | 116 | tape('test auth fails when 2nd packet is flipped', function (t) { 117 | bitflipTest(t, function (aliceHS, bobHS) { 118 | pull( 119 | aliceHS, 120 | bobHS, 121 | bitflipper(1), 122 | aliceHS 123 | ) 124 | }) 125 | }) 126 | 127 | tape('test auth fails when 3rd packet is flipped', function (t) { 128 | bitflipTest(t, function (aliceHS, bobHS) { 129 | pull( 130 | aliceHS, 131 | bitflipper(2), 132 | bobHS, 133 | aliceHS 134 | ) 135 | }) 136 | }) 137 | 138 | tape('test auth fails when 4th packet is flipped', function (t) { 139 | bitflipTest(t, function (aliceHS, bobHS) { 140 | pull( 141 | aliceHS, 142 | bobHS, 143 | bitflipper(2), 144 | aliceHS 145 | ) 146 | }) 147 | }) 148 | 149 | tape('test error cb when client is not authorized', function (t) { 150 | var errs = 0 151 | var aliceHS = shs.client(alice, app_key, 100) 152 | (bob.publicKey, function (err) { 153 | t.ok(err, 'Bob hungup') 154 | if(++errs === 2) t.end() 155 | }) 156 | 157 | var bobHS = shs.server(bob, unauthorized, app_key, 100) 158 | (function (err) { 159 | t.ok(err, 'client unauthorized') 160 | if(++errs === 2) t.end() 161 | }) 162 | 163 | pull(aliceHS, bobHS, aliceHS) 164 | }) 165 | 166 | tape('test error cb when client get wrong number', function (t) { 167 | var errs = 0 168 | var aliceHS = shs.client(alice, app_key, 100) 169 | ( 170 | // require('crypto').randomBytes(32) 171 | wally.publicKey 172 | , function (err) { 173 | t.ok(err, 'Bob hungup') 174 | if(++errs === 2) t.end() 175 | }) 176 | 177 | var bobHS = shs.server(bob, unauthorized, app_key, 100) 178 | (function (err) { 179 | t.ok(err, 'client unauthorized') 180 | if(++errs === 2) t.end() 181 | }) 182 | 183 | pull(aliceHS, bobHS, aliceHS) 184 | }) 185 | 186 | tape('test error cb when client get wrong number', function (t) { 187 | var errs = 0 188 | var aliceHS = shs.client(alice, app_key, 100) 189 | ( 190 | require('crypto').randomBytes(32) 191 | // wally.publicKey 192 | , function (err) { 193 | t.ok(err, 'connection failed') 194 | if(++errs === 2) t.end() 195 | }) 196 | 197 | var bobHS = shs.server(bob, unauthorized, app_key, 100) 198 | (function (err) { 199 | console.log(err) 200 | t.ok(err) 201 | if(++errs === 2) t.end() 202 | }) 203 | 204 | pull(aliceHS, bobHS, aliceHS) 205 | }) 206 | 207 | 208 | tape('error if created without public key', function (t) { 209 | 210 | var aliceHS = shs.client(alice, app_key, 100) 211 | t.throws(function () { 212 | aliceHS() 213 | }) 214 | t.end() 215 | }) 216 | 217 | tape('unauthorized connection must cb once', function (t) { 218 | t.plan(2) 219 | var n = 2 220 | var aliceHS = shs.client(alice, app_key, 100) 221 | var bobHS = shs.server(bob, authorized, app_key, 100) 222 | 223 | var as = aliceHS(bob.publicKey, function (err, stream) { 224 | console.log('Alice') 225 | t.ok(err, 'client connect should fail') 226 | next() 227 | }) 228 | 229 | pull(as, bobHS(function (err, stream) { 230 | console.log('Bob') 231 | t.ok(err, 'server connect should fail') 232 | next() 233 | }), as) 234 | 235 | function next () { 236 | if(--n) return 237 | t.end() 238 | } 239 | 240 | 241 | }) 242 | 243 | tape('client timeout error if there is no response', function (t) { 244 | 245 | var aliceHS = shs.client(alice, app_key, 100) 246 | (bob.publicKey, function (err, stream) { 247 | t.ok(err) 248 | t.end() 249 | }) 250 | 251 | pull( 252 | Hang(), 253 | aliceHS 254 | ) 255 | //do nothing, so aliceHS should timeout 256 | }) 257 | 258 | tape('server timeout error if there is no response', function (t) { 259 | 260 | var bobHS = shs.server(alice, authorized, app_key, 100) 261 | (function (err, stream) { 262 | t.ok(err) 263 | t.end() 264 | }) 265 | 266 | pull( 267 | Hang(), 268 | bobHS 269 | ) 270 | //do nothing, so aliceHS should timeout 271 | }) 272 | 273 | tape('test handshake', function (t) { 274 | 275 | var aliceHS = shs.client(null, app_key, 100) 276 | (bob.publicKey, hash('alice'), function (err, stream) { 277 | 278 | if(err) throw err 279 | 280 | }) 281 | 282 | var r = Math.random() 283 | var bobHS = shs.server(bob, function (publicKey, cb) { 284 | t.deepEqual(publicKey, alice.publicKey) 285 | cb(null, {okay: true, random: r}) 286 | }, app_key, 100) 287 | (function (err, stream) { 288 | if(err) throw err 289 | 290 | t.deepEqual(stream.auth, {okay: true, random: r}) 291 | t.end() 292 | }) 293 | 294 | pull( 295 | aliceHS, 296 | bobHS, 297 | aliceHS 298 | ) 299 | 300 | }) 301 | 302 | 303 | tape('toKeys', function (t) { 304 | t.deepEqual(shs.toKeys(hash('alice')), alice) 305 | t.deepEqual(shs.toKeys(alice), alice) 306 | t.end() 307 | }) 308 | 309 | 310 | 311 | 312 | 313 | -------------------------------------------------------------------------------- /test/shs1-test/client.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const pull = require('pull-stream') 4 | const toPull = require('stream-to-pull-stream') 5 | var sodium = require('chloride') 6 | const { createClient } = require('../..') 7 | 8 | const alice = sodium.crypto_sign_keypair() // client 9 | const appKey = Buffer.from(process.argv[2], 'hex') 10 | const bobPublicKey = Buffer.from(process.argv[3], 'hex') // server 11 | const seed = null // how do we get this fro shs1testclient ? 12 | 13 | const timeout = 10e3 // I hope this is milliseconds! 14 | 15 | const shake = createClient(alice, appKey, timeout)(bobPublicKey, seed, (err, stream) => { 16 | if (err) { 17 | log(`! ${err}`) 18 | process.exit(1) 19 | // shs1-test : If the server detects that the client is not well-behaved, 20 | // it must immediately exit with a non-zero exit code, without writing any further data to stdout. 21 | } 22 | 23 | const { encryptKey, decryptKey, encryptNonce, decryptNonce } = stream.crypto 24 | const result = Buffer.concat([ encryptKey, encryptNonce, decryptKey, decryptNonce ]) 25 | 26 | log(`< writing ${result.length} bytes (final)`) 27 | process.stdout.write(result) 28 | }) 29 | 30 | process.on('SIGTERM', () => { 31 | log('X received SIGTERM') 32 | }) 33 | 34 | pull( 35 | toPull.source(process.stdin), 36 | pull.through(data => log(`> reading ${data.length} bytes`)), 37 | shake, // duplex handshake stream 38 | pull.through(data => log(`< writing ${data.length} bytes`)), 39 | toPull.sink(process.stdout) 40 | ) 41 | 42 | function log (string) { 43 | // uncomment this to get some debugging data 44 | // process.stderr.write(`PID ${process.pid}: ${string}\n`) 45 | } 46 | -------------------------------------------------------------------------------- /test/shs1-test/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const pull = require('pull-stream') 4 | const toPull = require('stream-to-pull-stream') 5 | const { createServer } = require('../..') 6 | 7 | const appKey = Buffer.from(process.argv[2], 'hex') 8 | const bob = { // the keypair of the server 9 | secretKey: Buffer.from(process.argv[3], 'hex'), 10 | publicKey: Buffer.from(process.argv[4], 'hex') 11 | } 12 | const authorize = (pubKey, cb) => cb(null, true) // all clients are allowed to connect 13 | const timeout = 10e3 // I hope this is milliseconds! 14 | 15 | const shake = createServer(bob, authorize, appKey, timeout)((err, stream) => { 16 | if (err) { 17 | log(`! ${err}`) 18 | process.exit(1) 19 | // shs1-test : If the server detects that the client is not well-behaved, 20 | // it must immediately exit with a non-zero exit code, without writing any further data to stdout. 21 | } 22 | 23 | const { encryptKey, decryptKey, encryptNonce, decryptNonce } = stream.crypto 24 | const result = Buffer.concat([ encryptKey, encryptNonce, decryptKey, decryptNonce ]) 25 | 26 | log(`< writing ${result.length} bytes (final)`) 27 | process.stdout.write(result) 28 | }) 29 | 30 | process.on('SIGTERM', () => { 31 | log('X received SIGTERM') 32 | }) 33 | 34 | pull( 35 | toPull.source(process.stdin), 36 | pull.through(data => log(`> reading ${data.length} bytes`)), 37 | shake, // duplex handshake stream 38 | pull.through(data => log(`< writing ${data.length} bytes`)), 39 | toPull.sink(process.stdout) 40 | ) 41 | 42 | function log (string) { 43 | // uncomment this to get some debugging data 44 | // process.stderr.write(`PID ${process.pid}: ${string}\n`) 45 | } 46 | -------------------------------------------------------------------------------- /test/vectors.js: -------------------------------------------------------------------------------- 1 | 2 | require('test-secret-handshake/test')(require('../crypto')) 3 | --------------------------------------------------------------------------------