Skip to content

Commit

Permalink
Merge pull request #20 from paritytech/kirushik/more_compact_encoding
Browse files Browse the repository at this point in the history
Use base64 for shards encoding; print versions and commit hashes everywhere
  • Loading branch information
Kirill Pimenov committed Jul 15, 2019
2 parents eb3e145 + 27a2be7 commit 262b7ce
Show file tree
Hide file tree
Showing 9 changed files with 1,541 additions and 1,169 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: 2
jobs:
build:
docker:
- image: circleci/node:10
- image: circleci/node:12
working_directory: ~/repo
steps:
- checkout
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
"contributors": [
"Kirill Pimenov <kirill@parity.io>"
],

"version": "0.1.0",
"version": "0.2.0",
"private": true,

"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
Expand All @@ -17,6 +15,7 @@
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"base64-js": "^1.3.0",
"scryptsy": "^2.0.0",
"secrets.js-grempe": "^1.1.0",
"tweetnacl": "^1.0.0",
Expand Down
17 changes: 16 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
</div>
<SavePageInfo v-else-if="!localFile"/>
<GoOfflineInfo v-else-if="isOnline"/>
<p class="version-footer">BananaSplit version {{version}}, git revision {{gitRevision}}</p>
</div>
</template>

Expand All @@ -20,19 +21,27 @@ import GoOfflineInfo from './components/GoOfflineInfo'
import SavePageInfo from './components/SavePageInfo'
import ForkMe from './components/ForkMe'
import {version} from '../package.json';
export default {
name: 'App',
components: {GeneralInfo, GoOfflineInfo, SavePageInfo, ForkMe},
computed: {
localFile: function() {
return (window.location.protocol === 'file:');
return (window.location.protocol === 'file:')
},
secure: function() {
if (process.env.NODE_ENV === 'production') {
return this.localFile && !this.isOnline;
} else {
return true
}
},
version: function() {
return version
},
gitRevision: function() {
return process.env.GIT_REVISION;
}
}
}
Expand Down Expand Up @@ -67,4 +76,10 @@ nav a.router-link-exact-active {
display: none;
}
}
.version-footer {
font-size: 80%;
font-style: italic;
color: darkgray;
}
</style>
15 changes: 14 additions & 1 deletion src/components/ShardQrCode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@
<h3>You need {{requiredShards - 1}} more QR {{pluralizeCode}} like this to reconstruct the secret</h3>
<h4>Please go to <a href="https://bs.parity.io">https://bs.parity.io</a> to download the reconstruction webapp, if you don't have one already</h4>
</div>
<qriously class="print-only" v-bind:value="shard" v-bind:size="600" />
<qriously class="print-only" v-bind:value="shard" v-bind:size="750" />
<qriously class="screen-only" v-bind:value="shard" v-bind:size="200" />
<div class="print-only">
<div class="recovery-field">
<div class="recovery-title">Recovery&nbsp;passphrase&nbsp;is&nbsp;</div>
<div class="recovery-blank"/>
</div>
<p class="version">This has been generated by BananaSplit version {{version}}, git revision {{gitRevision}}</p>
</div>
</div>
</template>

<script>
import {version} from '../../package.json';
export default {
name: 'ShardQrCode',
props: {
Expand All @@ -31,6 +33,12 @@ export default {
} else {
return 'codes'
}
},
version: function() {
return version;
},
gitRevision: function() {
return process.env.GIT_REVISION;
}
}
}
Expand Down Expand Up @@ -66,4 +74,9 @@ export default {
width: 100%;
border-bottom: 1px solid black;
}
.version {
font-style: italic;
color: darkgray;
}
</style>
42 changes: 33 additions & 9 deletions src/util/crypto.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const BASE64 = require("base64-js");
const CRYPTO = require("tweetnacl");
const SCRYPT = require("scryptsy");

Expand Down Expand Up @@ -55,13 +56,17 @@ function decrypt(data, salt, passphrase, nonce) {
function share(data, title, passphrase, totalShards, requiredShards) {
var salt = hashString(title);
var encrypted = encrypt(data, salt, passphrase);
var nonce = hexify(encrypted.nonce);
var nonce = BASE64.fromByteArray(encrypted.nonce);
var hexEncrypted = hexify(encrypted.value);
return SECRETS.share(hexEncrypted, totalShards, requiredShards).map(function (shard) {
// First char is non-hex (base36) and signifies the bitfield size of our share
var encodedShard = shard[0] + BASE64.fromByteArray(dehexify(shard.slice(1)));

return JSON.stringify({
v: 1,
t: title,
r: requiredShards,
d: shard,
d: encodedShard,
n: nonce
}).replace(/[\u007F-\uFFFF]/g, function (chr) {
return "\\u" + ("0000" + chr.charCodeAt(0).toString(16)).substr(-4)
Expand All @@ -72,6 +77,7 @@ function share(data, title, passphrase, totalShards, requiredShards) {
function parse(payload) {
let parsed = JSON.parse(payload);
return {
version: parsed.v || 0, // 'undefined' version is treated as 0
title: parsed.t,
requiredShards: parsed.r,
data: parsed.d,
Expand All @@ -80,8 +86,6 @@ function parse(payload) {
}

function reconstruct(shardObjects, passphrase) {
var shardData = shardObjects.map(shard => shard.data);

var shardsRequirements = shardObjects.map(shard => shard.requiredShards);
if (!shardsRequirements.every(r => r===shardsRequirements[0])) {
throw "Mismatching min shards requirement among shards!"
Expand All @@ -100,11 +104,31 @@ function reconstruct(shardObjects, passphrase) {
throw "Titles mismatch among shards!"
}

var encryptedSecret = SECRETS.combine(shardData);
var secret = dehexify(encryptedSecret);
var nonce = dehexify(nonces[0]);
var salt = hashString(titles[0]);
return uint8ArrayToStr(decrypt(secret, salt, passphrase, nonce));
var versions = shardObjects.map(shard => shard.version);
if (!versions.every(v => v===versions[0])) {
throw "Versions mismatch along shards!"
}

switch (versions[0]) {
case 0:
var shardData = shardObjects.map(shard => shard.data);
var encryptedSecret = SECRETS.combine(shardData);
var secret = dehexify(encryptedSecret);
var nonce = dehexify(nonces[0]);
var salt = hashString(titles[0]);
return uint8ArrayToStr(decrypt(secret, salt, passphrase, nonce));

case 1:
var shardDataV1 = shardObjects.map(shard => shard.data[0]+hexify(BASE64.toByteArray(shard.data.slice(1))));
var encryptedSecretV1 = SECRETS.combine(shardDataV1);
var secretV1 = dehexify(encryptedSecretV1);
var nonceV1 = BASE64.toByteArray(nonces[0]);
var saltV1 = hashString(titles[0]);
return uint8ArrayToStr(decrypt(secretV1, saltV1, passphrase, nonceV1));

default:
throw "Version is not supported!";
}
}

export default {
Expand Down
14 changes: 11 additions & 3 deletions src/views/Share.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
<div id="share-controls">
<h1>Share secrets</h1>
<p>What is this thing? <input type="text" :disabled="encryptionMode" v-model="title" placeholder="Like, 'Bitcoin seed phrase'" autofocus/></p>
<textarea v-model="secret" :disabled="encryptionMode" placeholder="Your secret goes here"></textarea>
<textarea v-model="secret" v-bind:class="{tooLong: secretTooLong}" :disabled="encryptionMode" placeholder="Your secret goes here"></textarea>
<div v-if="this.secretTooLong">Inputs longer than 1024 characters make QR codes illegible</div>
<p>Will require any {{requiredShards}} shards out of <input type="number" v-model.number="totalShards" min="3" />
to reconstruct</p>
<button v-on:click="toggleMode">
<button :disabled="secretTooLong" v-on:click="toggleMode">
<span v-if="this.encryptionMode">Back to editing data</span>
<span v-else>Generate QR codes!</span>
</button>
Expand Down Expand Up @@ -42,11 +43,14 @@ export default {
secret: '',
totalShards: 3, // TODO: 5
recoveryPassphrase: bipPhrase.generate(4),
encryptionMode: false,
encryptionMode: false
}
},
components: { ShardInfo, CanvasText },
computed: {
secretTooLong: function() {
return this.secret.length > 1024;
},
requiredShards: function () {
return Math.floor(this.totalShards / 2) + 1;
},
Expand Down Expand Up @@ -75,6 +79,10 @@ export default {
</script>

<style>
textarea.tooLong {
border: 5px solid red;
}
#qr-tiles {
display: flex;
flex-wrap: wrap;
Expand Down
15 changes: 15 additions & 0 deletions tests/unit/crypto.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@ test('reconstructs the reference example of v0 serialization', () => {
});
})

test('reconstructs the reference example of v1 serialization', () => {
var shards = [
'{"v":1,"t":"Pssst","r":2,"d":"8AdI3F1Xn4CK9mXEVWLWgg2gzho5WV38E/hn1OYyRMZenL/Jm6dmrZoiji2ZlMSVEW+XN9WW1I/ilDC1yiu4oBa4=","n":"a17TDZHP2iL/sdPHgFJUP3NlAC7bDgrp"}',
'{"v":1,"t":"Pssst","r":2,"d":"8ArluLqrT3URnL+IqsHddG9MpXqvSNt5JBLfTqSJmCg4raIFLg2XhfbnFLCTgCumI4qByThq9bBxnwLy8EgEHYiw=","n":"a17TDZHP2iL/sdPHgFJUP3NlAC7bDgrp"}',
'{"v":1,"t":"Pssst","r":2,"d":"8A2tZOf80PWbatpM/6ML9mLrUFkOu4kpyUiY62bPA6HmkVVtQpfosdF3nuhpo6K3MfmjsJ8ROokDShDgNka/ptFI=","n":"a17TDZHP2iL/sdPHgFJUP3NlAC7bDgrp"}'
].map(s => crypto.parse(s));

[
[0, 1], [0, 2], [1, 2],
[1, 0], [2, 0], [2, 1]
].forEach(([i,j]) => {
expect(crypto.reconstruct([shards[i], shards[j]], 'excess-torch-unfold-fix')).toBe('Version one is all-around better')
});
})

test('throws an error if nonces mismatch', () => {
var shards = crypto.share('Message', 'Title', 'correct-horse-battery-staple', 3, 2).map(s => crypto.parse(s));
shards[0].nonce = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
Expand Down
11 changes: 10 additions & 1 deletion vue.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
let HtmlWebpackPlugin = require('html-webpack-plugin');
let HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
let Webpack = require('webpack');

let childProcess = require('child_process');
let GIT_REVISION = childProcess.execSync('git rev-parse HEAD').toString();

module.exports = {
productionSourceMap: false,
Expand All @@ -12,7 +16,12 @@ module.exports = {
template: 'public/index.html',
inlineSource: '.(js|css)$'
}),
new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin)
new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin),
new Webpack.DefinePlugin({
'process.env': {
'GIT_REVISION': JSON.stringify(GIT_REVISION)
}
})
]
}
}
Loading

0 comments on commit 262b7ce

Please sign in to comment.