├── .gitignore ├── isString.js ├── .travis.yml ├── v1.js ├── index.js ├── package.json ├── test ├── v1.test.js └── v2.test.js ├── v2.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /isString.js: -------------------------------------------------------------------------------- 1 | module.exports = function isString (variable) { 2 | return variable && typeof variable === 'string' 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | before_install: 6 | - npm install -g npm@latest 7 | - sudo apt-get -qq update 8 | 9 | node_js: 10 | - 8 11 | 12 | script: 13 | - npm test 14 | -------------------------------------------------------------------------------- /v1.js: -------------------------------------------------------------------------------- 1 | const secrets = require('secrets.js-grempe') 2 | 3 | module.exports = { 4 | pack: function packV1 (secret) { 5 | return secret 6 | }, 7 | unpack: function unpackV1 (secret) { 8 | return { secret } 9 | }, 10 | share: function shareV1 (secret, numOfShards, quorum) { 11 | const hexSecret = secrets.str2hex(secret) 12 | return secrets.share(hexSecret, numOfShards, quorum) 13 | }, 14 | combine: function combineV1 (shards) { 15 | const hex = secrets.combine(shards) 16 | return secrets.hex2str(hex) 17 | }, 18 | verify: function verifyV1 (shards) { 19 | try { 20 | throw new Error("version 1.0.0 doesn't support secret verification") 21 | } catch (err) { 22 | return false 23 | } 24 | }, 25 | validateShard: function validateShardV1 (shard) { 26 | try { 27 | secrets.extractShareComponents(shard) 28 | } catch (err) { 29 | return false 30 | } 31 | return true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const v2 = require('./v2') 2 | const v1 = require('./v1') 3 | 4 | module.exports = { 5 | pack: v2.pack, 6 | unpack: function unpack (secret, version) { 7 | switch (version) { 8 | case '2.0.0': return v2.unpack(secret) 9 | case '1.0.0': return v1.unpack(secret) 10 | default: return false 11 | } 12 | }, 13 | share: v2.share, 14 | combine: function combine (shards, version) { 15 | switch (version) { 16 | case '2.0.0': return v2.combine(shards) 17 | case '1.0.0': return v1.combine(shards) 18 | default: return false 19 | } 20 | }, 21 | verify: function verify (shards, version) { 22 | switch (version) { 23 | case '2.0.0': return v2.verify(shards) 24 | case '1.0.0': return v1.verify(shards) 25 | default: return false 26 | } 27 | }, 28 | validateShard: function validateShard (shard, version) { 29 | switch (version) { 30 | case '2.0.0': return v2.validateShard(shard) 31 | case '1.0.0': return v1.validateShard(shard) 32 | default: return false 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dark-crystal-secrets", 3 | "version": "1.0.0", 4 | "description": "Dark Crystal's wrapper around Shamir's Secret Shares", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run test:js && npm run test:lint", 8 | "test:js": "tape 'test/**/*.test.js' | tap-spec", 9 | "test:lint": "standard", 10 | "lint": "standard --fix" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/blockades/dark-crystal-secrets.git" 15 | }, 16 | "keywords": [ 17 | "shamirs", 18 | "secret", 19 | "shares", 20 | "dark-crystal" 21 | ], 22 | "author": "kyphae", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/blockades/dark-crystal-secrets/issues" 26 | }, 27 | "homepage": "https://github.com/blockades/dark-crystal-secrets#readme", 28 | "dependencies": { 29 | "secrets.js-grempe": "^1.1.0", 30 | "sodium-native": "^2.2.4" 31 | }, 32 | "devDependencies": { 33 | "standard": "^12.0.1", 34 | "tap-spec": "^5.0.0", 35 | "tape": "^4.9.2", 36 | "tape-plus": "^1.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/v1.test.js: -------------------------------------------------------------------------------- 1 | const { describe } = require('tape-plus') 2 | 3 | const { share, combine } = require('../v1') 4 | 5 | describe('secrets-wrapper (v1)', context => { 6 | let secret, numRecps, quorum 7 | 8 | context.beforeEach(c => { 9 | secret = Math.random().toString(36) 10 | numRecps = 5 11 | quorum = 3 12 | }) 13 | 14 | context('secret can be reproduced from quorum of shards', (assert, next) => { 15 | try { 16 | const shards = share(secret, numRecps, quorum).slice(2) 17 | var result = combine(shards) 18 | } catch (err) { 19 | assert.notOk(err, 'does not throw an error') 20 | } 21 | assert.equal(result, secret, 'secret recovered') 22 | next() 23 | }) 24 | 25 | context('secret cannot be reproduced when an invalid shard is given', (assert, next) => { 26 | try { 27 | var shards = share(secret, numRecps, quorum) 28 | shards[1] = 'this is not a shard' 29 | var result = combine(shards) 30 | } catch (err) { 31 | assert.ok(err, 'throws an error') 32 | } 33 | assert.notEqual(result, secret, 'secret not recovered') 34 | next() 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /test/v2.test.js: -------------------------------------------------------------------------------- 1 | const { describe } = require('tape-plus') 2 | const { share, combine, verify } = require('../v2') 3 | 4 | describe('secrets-wrapper (v2)', context => { 5 | let secret, numRecps, quorum 6 | 7 | context.beforeEach(c => { 8 | secret = Math.random().toString(36) 9 | numRecps = 5 10 | quorum = 3 11 | }) 12 | 13 | context('secret can be reproduced from quorum of shards', (assert, next) => { 14 | try { 15 | const shards = share(secret, numRecps, quorum).slice(2) 16 | var result = combine(shards) 17 | } catch (err) { 18 | assert.notOk(err, 'does not throw an error') 19 | } 20 | assert.equal(result, secret, 'secret recovered') 21 | next() 22 | }) 23 | 24 | context('shards can be used to checksig of a secret', (assert, next) => { 25 | const shards = share(secret, numRecps, quorum) 26 | var result = verify(shards) 27 | assert.ok(result) 28 | next() 29 | }) 30 | 31 | context('secret cannot be reproduced from less than quorum of shards', (assert, next) => { 32 | try { 33 | const shards = share(secret, numRecps, quorum).slice(3) 34 | var result = combine(shards) 35 | } catch (err) { 36 | assert.ok(err, 'throws an error') 37 | } 38 | assert.notEqual(result, secret, 'secret not recovered') 39 | next() 40 | }) 41 | 42 | context('secret cannot be reproduced when an invalid shard is given', (assert, next) => { 43 | try { 44 | var shards = share(secret, numRecps, quorum) 45 | shards[1] = 'this is not a shard' 46 | var result = combine(shards) 47 | } catch (err) { 48 | assert.ok(err, 'throws an error') 49 | } 50 | assert.notEqual(result, secret, 'secret not recovered') 51 | next() 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /v2.js: -------------------------------------------------------------------------------- 1 | const secrets = require('secrets.js-grempe') 2 | const sodium = require('sodium-native') 3 | const isString = require('./isString') 4 | 5 | const MAC_LENGTH = 32 6 | 7 | module.exports = { 8 | pack: function packV2 (secret, label) { 9 | return JSON.stringify([secret, label]) 10 | }, 11 | unpack: function unpackV2 (secret) { 12 | let arr 13 | try { 14 | arr = JSON.parse(secret) 15 | 16 | if ((arr.length !== 2) || (!arr.every(isString))) return false 17 | } catch (err) { 18 | return false 19 | } 20 | return { secret: arr[0], label: arr[1] } 21 | }, 22 | share: function shareV2 (secret, numOfShards, quorum) { 23 | const shardsHex = secrets.share(secret2Hex(secret), numOfShards, quorum) 24 | return shardsHex.map(compress) 25 | }, 26 | combine: function combineV2 (shards) { 27 | // this could probably be improved by checking the hash before converting to hex 28 | const hex = secrets.combine(shards.map(decompress)) 29 | return hex2Secret(hex) 30 | }, 31 | verify: function verifyV2 (shards) { 32 | const hex = secrets.combine(shards.map(decompress)) 33 | const secret = secrets.hex2str(hex.slice(MAC_LENGTH)) 34 | if (Mac(secret) !== mac(hex)) return false 35 | else return true 36 | }, 37 | validateShard: function validateShardV2 (shard) { 38 | try { 39 | secrets.extractShareComponents(decompress(shard)) 40 | } catch (err) { 41 | return false 42 | } 43 | return true 44 | } 45 | } 46 | 47 | // helpers which prepare a secret ready for sharding, and also manage the MAC 48 | // TODO - could be better names 49 | 50 | // secret-specific mac 51 | function mac (hex) { 52 | return hex.slice(0, MAC_LENGTH) 53 | } 54 | 55 | function secret2Hex (string) { 56 | return Mac(string) + secrets.str2hex(string) 57 | } 58 | 59 | function hex2Secret (hex) { 60 | const mac = hex.slice(0, MAC_LENGTH) 61 | const secret = secrets.hex2str(hex.slice(MAC_LENGTH)) 62 | 63 | if (Mac(secret) !== mac) throw new Error('This secret appears to be corrupt - invalid MAC') 64 | else return secret 65 | } 66 | 67 | // supposedly 'original' mac 68 | function Mac (secret) { 69 | var hash = Buffer.alloc(sodium.crypto_hash_sha256_BYTES) 70 | sodium.crypto_hash_sha256(hash, Buffer.from(secret)) 71 | 72 | return hash 73 | .toString('hex') 74 | .slice(-1 * MAC_LENGTH) 75 | } 76 | 77 | // compress shards by using denser encoding 78 | 79 | function compress (shard) { 80 | const shardData = shard.slice(3) 81 | const shardDataBase64 = Buffer.from(shardData, 'hex').toString('base64') 82 | return shard.slice(0, 3) + shardDataBase64 83 | } 84 | 85 | function decompress (shard) { 86 | const shardData = shard.slice(3) 87 | const shardDataHex = Buffer.from(shardData, 'base64').toString('hex') 88 | return shard.slice(0, 3) + shardDataHex 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dark Crystal Secrets 2 | 3 | [Dark Crystal's](https://darkcrystal.pw) encryption wrapper around Shamir's Secret Shares implementation [`secrets.js-grempe`](https://github.com/grempe/secrets.js). 4 | 5 | Using a semantic versioning system, this module also provides backward compatibility to Dark Crystal records out there in the wild. This is required as we incrementally update Dark Crystal's encryption schemes, exploring, experimenting and improving our implementation. 6 | 7 |
8 |
9 |