├── .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 | dark-crystal-secrets 9 |

10 | 11 | ## Example 12 | 13 | ```js 14 | const { pack, unpack. share, verify, combine } = require('dark-crystal-secretes') 15 | 16 | const labelledSecret = pack('burried under my tree fort', 'treasure chest 1') 17 | const shares = share(labelledSecret, 5, 3) // split into 5 parts, quorum 3 18 | 19 | shares.forEach(share => console.log(validateShard(share, '2.0.0')) 20 | 21 | const validity = verify(share.slice(0,3), '2.0.0') 22 | console.log(validity) 23 | 24 | const recoveredLabelledSecret = combine(share.slice(0,3), '2.0.0') 25 | const { secret, label } = unpack(recoveredLabelledSecret, '2.0.0') 26 | ``` 27 | 28 | ## API 29 | 30 | #### `pack(secret, label)` 31 | The `secret` is bundled up with the `label` given to the secret and stringified as `JSON` 32 | 33 | #### `unpack(secret, version)` 34 | The `secret` is separated from the `label` and returned as an object 35 | 36 | #### `share(secret, numOfShards, quorum)` 37 | * Generates a `MAC` which is composed the first 32 characters (16 bytes) of a `SHA256` hash of the `secret` 38 | * Concatenates it at the beginning of the `secret` 39 | * Splits the secret into `numOfShards`, where `quorum` is the number required to reassemble 40 | * Compresses the `shards` from `hex` into `base64` for more efficient storage 41 | 42 | #### `verify(shards, version)` 43 | * Decompresses the `shards` from `base64` to `hex` 44 | * Reassembles the secret 45 | * Checksig 46 | * `MAC` generated from the newly returned secret match the `MAC` attached to the returned secret. 47 | * Returns `false` if fails to pass check 48 | 49 | #### `combine(shards, version)` 50 | * Decompresses the shards from base64 to hex 51 | * Reassembles the secret 52 | * Checksig 53 | * `MAC` generated from the newly returned secret match the `MAC` attached to the returned secret. 54 | * Throws an error if it fails to pass the check 55 | * Returns the `JSON` string secret 56 | 57 | #### `validateShard(shard, version)` 58 | * Decompresses the shards from base64 to hex 59 | * Extracts components of the shard 60 | * `bits`: [Number] The number of bits configured when the share was created. 61 | * `id`: [Number] The ID number associated with the share when created. 62 | * `data`: [String] A hex string of the actual share data. 63 | --------------------------------------------------------------------------------