Skip to content

Commit 262b7ce

Browse files
author
Kirill Pimenov
authored
Merge pull request #20 from paritytech/kirushik/more_compact_encoding
Use base64 for shards encoding; print versions and commit hashes everywhere
2 parents eb3e145 + 27a2be7 commit 262b7ce

File tree

9 files changed

+1541
-1169
lines changed

9 files changed

+1541
-1169
lines changed

.circleci/config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ version: 2
22
jobs:
33
build:
44
docker:
5-
- image: circleci/node:10
5+
- image: circleci/node:12
66
working_directory: ~/repo
77
steps:
88
- checkout

package.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@
55
"contributors": [
66
"Kirill Pimenov <[email protected]>"
77
],
8-
9-
"version": "0.1.0",
8+
"version": "0.2.0",
109
"private": true,
11-
1210
"scripts": {
1311
"serve": "vue-cli-service serve",
1412
"build": "vue-cli-service build",
@@ -17,6 +15,7 @@
1715
"test:unit": "vue-cli-service test:unit"
1816
},
1917
"dependencies": {
18+
"base64-js": "^1.3.0",
2019
"scryptsy": "^2.0.0",
2120
"secrets.js-grempe": "^1.1.0",
2221
"tweetnacl": "^1.0.0",

src/App.vue

+16-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
</div>
1212
<SavePageInfo v-else-if="!localFile"/>
1313
<GoOfflineInfo v-else-if="isOnline"/>
14+
<p class="version-footer">BananaSplit version {{version}}, git revision {{gitRevision}}</p>
1415
</div>
1516
</template>
1617

@@ -20,19 +21,27 @@ import GoOfflineInfo from './components/GoOfflineInfo'
2021
import SavePageInfo from './components/SavePageInfo'
2122
import ForkMe from './components/ForkMe'
2223
24+
import {version} from '../package.json';
25+
2326
export default {
2427
name: 'App',
2528
components: {GeneralInfo, GoOfflineInfo, SavePageInfo, ForkMe},
2629
computed: {
2730
localFile: function() {
28-
return (window.location.protocol === 'file:');
31+
return (window.location.protocol === 'file:')
2932
},
3033
secure: function() {
3134
if (process.env.NODE_ENV === 'production') {
3235
return this.localFile && !this.isOnline;
3336
} else {
3437
return true
3538
}
39+
},
40+
version: function() {
41+
return version
42+
},
43+
gitRevision: function() {
44+
return process.env.GIT_REVISION;
3645
}
3746
}
3847
}
@@ -67,4 +76,10 @@ nav a.router-link-exact-active {
6776
display: none;
6877
}
6978
}
79+
80+
.version-footer {
81+
font-size: 80%;
82+
font-style: italic;
83+
color: darkgray;
84+
}
7085
</style>

src/components/ShardQrCode.vue

+14-1
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@
55
<h3>You need {{requiredShards - 1}} more QR {{pluralizeCode}} like this to reconstruct the secret</h3>
66
<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>
77
</div>
8-
<qriously class="print-only" v-bind:value="shard" v-bind:size="600" />
8+
<qriously class="print-only" v-bind:value="shard" v-bind:size="750" />
99
<qriously class="screen-only" v-bind:value="shard" v-bind:size="200" />
1010
<div class="print-only">
1111
<div class="recovery-field">
1212
<div class="recovery-title">Recovery&nbsp;passphrase&nbsp;is&nbsp;</div>
1313
<div class="recovery-blank"/>
1414
</div>
15+
<p class="version">This has been generated by BananaSplit version {{version}}, git revision {{gitRevision}}</p>
1516
</div>
1617
</div>
1718
</template>
1819

1920
<script>
21+
import {version} from '../../package.json';
2022
export default {
2123
name: 'ShardQrCode',
2224
props: {
@@ -31,6 +33,12 @@ export default {
3133
} else {
3234
return 'codes'
3335
}
36+
},
37+
version: function() {
38+
return version;
39+
},
40+
gitRevision: function() {
41+
return process.env.GIT_REVISION;
3442
}
3543
}
3644
}
@@ -66,4 +74,9 @@ export default {
6674
width: 100%;
6775
border-bottom: 1px solid black;
6876
}
77+
78+
.version {
79+
font-style: italic;
80+
color: darkgray;
81+
}
6982
</style>

src/util/crypto.js

+33-9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const BASE64 = require("base64-js");
12
const CRYPTO = require("tweetnacl");
23
const SCRYPT = require("scryptsy");
34

@@ -55,13 +56,17 @@ function decrypt(data, salt, passphrase, nonce) {
5556
function share(data, title, passphrase, totalShards, requiredShards) {
5657
var salt = hashString(title);
5758
var encrypted = encrypt(data, salt, passphrase);
58-
var nonce = hexify(encrypted.nonce);
59+
var nonce = BASE64.fromByteArray(encrypted.nonce);
5960
var hexEncrypted = hexify(encrypted.value);
6061
return SECRETS.share(hexEncrypted, totalShards, requiredShards).map(function (shard) {
62+
// First char is non-hex (base36) and signifies the bitfield size of our share
63+
var encodedShard = shard[0] + BASE64.fromByteArray(dehexify(shard.slice(1)));
64+
6165
return JSON.stringify({
66+
v: 1,
6267
t: title,
6368
r: requiredShards,
64-
d: shard,
69+
d: encodedShard,
6570
n: nonce
6671
}).replace(/[\u007F-\uFFFF]/g, function (chr) {
6772
return "\\u" + ("0000" + chr.charCodeAt(0).toString(16)).substr(-4)
@@ -72,6 +77,7 @@ function share(data, title, passphrase, totalShards, requiredShards) {
7277
function parse(payload) {
7378
let parsed = JSON.parse(payload);
7479
return {
80+
version: parsed.v || 0, // 'undefined' version is treated as 0
7581
title: parsed.t,
7682
requiredShards: parsed.r,
7783
data: parsed.d,
@@ -80,8 +86,6 @@ function parse(payload) {
8086
}
8187

8288
function reconstruct(shardObjects, passphrase) {
83-
var shardData = shardObjects.map(shard => shard.data);
84-
8589
var shardsRequirements = shardObjects.map(shard => shard.requiredShards);
8690
if (!shardsRequirements.every(r => r===shardsRequirements[0])) {
8791
throw "Mismatching min shards requirement among shards!"
@@ -100,11 +104,31 @@ function reconstruct(shardObjects, passphrase) {
100104
throw "Titles mismatch among shards!"
101105
}
102106

103-
var encryptedSecret = SECRETS.combine(shardData);
104-
var secret = dehexify(encryptedSecret);
105-
var nonce = dehexify(nonces[0]);
106-
var salt = hashString(titles[0]);
107-
return uint8ArrayToStr(decrypt(secret, salt, passphrase, nonce));
107+
var versions = shardObjects.map(shard => shard.version);
108+
if (!versions.every(v => v===versions[0])) {
109+
throw "Versions mismatch along shards!"
110+
}
111+
112+
switch (versions[0]) {
113+
case 0:
114+
var shardData = shardObjects.map(shard => shard.data);
115+
var encryptedSecret = SECRETS.combine(shardData);
116+
var secret = dehexify(encryptedSecret);
117+
var nonce = dehexify(nonces[0]);
118+
var salt = hashString(titles[0]);
119+
return uint8ArrayToStr(decrypt(secret, salt, passphrase, nonce));
120+
121+
case 1:
122+
var shardDataV1 = shardObjects.map(shard => shard.data[0]+hexify(BASE64.toByteArray(shard.data.slice(1))));
123+
var encryptedSecretV1 = SECRETS.combine(shardDataV1);
124+
var secretV1 = dehexify(encryptedSecretV1);
125+
var nonceV1 = BASE64.toByteArray(nonces[0]);
126+
var saltV1 = hashString(titles[0]);
127+
return uint8ArrayToStr(decrypt(secretV1, saltV1, passphrase, nonceV1));
128+
129+
default:
130+
throw "Version is not supported!";
131+
}
108132
}
109133

110134
export default {

src/views/Share.vue

+11-3
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
<div id="share-controls">
44
<h1>Share secrets</h1>
55
<p>What is this thing? <input type="text" :disabled="encryptionMode" v-model="title" placeholder="Like, 'Bitcoin seed phrase'" autofocus/></p>
6-
<textarea v-model="secret" :disabled="encryptionMode" placeholder="Your secret goes here"></textarea>
6+
<textarea v-model="secret" v-bind:class="{tooLong: secretTooLong}" :disabled="encryptionMode" placeholder="Your secret goes here"></textarea>
7+
<div v-if="this.secretTooLong">Inputs longer than 1024 characters make QR codes illegible</div>
78
<p>Will require any {{requiredShards}} shards out of <input type="number" v-model.number="totalShards" min="3" />
89
to reconstruct</p>
9-
<button v-on:click="toggleMode">
10+
<button :disabled="secretTooLong" v-on:click="toggleMode">
1011
<span v-if="this.encryptionMode">Back to editing data</span>
1112
<span v-else>Generate QR codes!</span>
1213
</button>
@@ -42,11 +43,14 @@ export default {
4243
secret: '',
4344
totalShards: 3, // TODO: 5
4445
recoveryPassphrase: bipPhrase.generate(4),
45-
encryptionMode: false,
46+
encryptionMode: false
4647
}
4748
},
4849
components: { ShardInfo, CanvasText },
4950
computed: {
51+
secretTooLong: function() {
52+
return this.secret.length > 1024;
53+
},
5054
requiredShards: function () {
5155
return Math.floor(this.totalShards / 2) + 1;
5256
},
@@ -75,6 +79,10 @@ export default {
7579
</script>
7680

7781
<style>
82+
textarea.tooLong {
83+
border: 5px solid red;
84+
}
85+
7886
#qr-tiles {
7987
display: flex;
8088
flex-wrap: wrap;

tests/unit/crypto.spec.js

+15
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,21 @@ test('reconstructs the reference example of v0 serialization', () => {
6565
});
6666
})
6767

68+
test('reconstructs the reference example of v1 serialization', () => {
69+
var shards = [
70+
'{"v":1,"t":"Pssst","r":2,"d":"8AdI3F1Xn4CK9mXEVWLWgg2gzho5WV38E/hn1OYyRMZenL/Jm6dmrZoiji2ZlMSVEW+XN9WW1I/ilDC1yiu4oBa4=","n":"a17TDZHP2iL/sdPHgFJUP3NlAC7bDgrp"}',
71+
'{"v":1,"t":"Pssst","r":2,"d":"8ArluLqrT3URnL+IqsHddG9MpXqvSNt5JBLfTqSJmCg4raIFLg2XhfbnFLCTgCumI4qByThq9bBxnwLy8EgEHYiw=","n":"a17TDZHP2iL/sdPHgFJUP3NlAC7bDgrp"}',
72+
'{"v":1,"t":"Pssst","r":2,"d":"8A2tZOf80PWbatpM/6ML9mLrUFkOu4kpyUiY62bPA6HmkVVtQpfosdF3nuhpo6K3MfmjsJ8ROokDShDgNka/ptFI=","n":"a17TDZHP2iL/sdPHgFJUP3NlAC7bDgrp"}'
73+
].map(s => crypto.parse(s));
74+
75+
[
76+
[0, 1], [0, 2], [1, 2],
77+
[1, 0], [2, 0], [2, 1]
78+
].forEach(([i,j]) => {
79+
expect(crypto.reconstruct([shards[i], shards[j]], 'excess-torch-unfold-fix')).toBe('Version one is all-around better')
80+
});
81+
})
82+
6883
test('throws an error if nonces mismatch', () => {
6984
var shards = crypto.share('Message', 'Title', 'correct-horse-battery-staple', 3, 2).map(s => crypto.parse(s));
7085
shards[0].nonce = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

vue.config.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
let HtmlWebpackPlugin = require('html-webpack-plugin');
22
let HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
3+
let Webpack = require('webpack');
4+
5+
let childProcess = require('child_process');
6+
let GIT_REVISION = childProcess.execSync('git rev-parse HEAD').toString();
37

48
module.exports = {
59
productionSourceMap: false,
@@ -12,7 +16,12 @@ module.exports = {
1216
template: 'public/index.html',
1317
inlineSource: '.(js|css)$'
1418
}),
15-
new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin)
19+
new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin),
20+
new Webpack.DefinePlugin({
21+
'process.env': {
22+
'GIT_REVISION': JSON.stringify(GIT_REVISION)
23+
}
24+
})
1625
]
1726
}
1827
}

0 commit comments

Comments
 (0)