Fix linter issues

Now that the rules are much more pedantic there are quite some places
where we are inconsistent with codestyle; all security issues detected
were false positives, though.
This commit is contained in:
Kirill Pimenov 2020-08-03 20:38:37 +02:00
parent ff93057bd3
commit e27865e53b
No known key found for this signature in database
GPG Key ID: FBF0C4CB6A375D79
18 changed files with 2591 additions and 444 deletions

View File

@ -18,5 +18,24 @@ module.exports = {
env: {
browser: true,
node: true
},
rules: {
"vue/html-self-closing": [
"warning",
{
html: "never"
}
],
"vue/v-on-style": ["warning", "longform"],
"vue/max-attributes-per-line": [
"warning",
{
singleline: 3,
multiline: {
max: 1,
allowFirstLine: true
}
}
]
}
};

View File

@ -1,24 +1,18 @@
module.exports = {
moduleFileExtensions: [
'js',
'jsx',
'json',
'vue'
],
moduleFileExtensions: ["js", "jsx", "json", "vue"],
transform: {
'^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
"^.+\\.vue$": "vue-jest",
".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$":
"jest-transform-stub",
"^.+\\.jsx?$": "babel-jest"
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
"^@/(.*)$": "<rootDir>/src/$1"
},
snapshotSerializers: [
'jest-serializer-vue'
],
snapshotSerializers: ["jest-serializer-vue"],
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
"**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"
],
testURL: 'http://localhost/',
reporters: [ 'default', 'jest-junit' ]
}
testURL: "http://localhost/",
reporters: ["default", "jest-junit"]
};

View File

@ -5,8 +5,12 @@
<div v-if="secure">
<router-view />
<nav>
<router-link to="/share">Create</router-link>
<router-link to="/combine">Restore </router-link>
<router-link to="/share">
Create
</router-link>
<router-link to="/combine">
Restore
</router-link>
</nav>
</div>
<SavePageInfo v-else-if="!localFile" />

View File

@ -1,30 +1,33 @@
<template>
<canvas class="canvasText" width="0" height="28px" v-canvas-message="text"></canvas>
<canvas v-canvas-message="text" class="canvasText" width="0" height="28px" />
</template>
<script>
export default {
name: 'CanvasText',
props: {
text: String
},
directives: {
canvasMessage: function(canvasElement, binding) {
var context = canvasElement.getContext("2d");
context.clearRect(0, 0, canvasElement.width, canvasElement.height);
context.fillStyle = "black";
context.font = "20px Arial";
var textSize = context.measureText(binding.value);
canvasElement.setAttribute("width", (textSize.width+30)+"px");
context.font = "20px Arial";
context.fillText(binding.value, 15, 20)
}
name: "CanvasText",
directives: {
canvasMessage: function(canvasElement, binding) {
var context = canvasElement.getContext("2d");
context.clearRect(0, 0, canvasElement.width, canvasElement.height);
context.fillStyle = "black";
context.font = "20px Arial";
var textSize = context.measureText(binding.value);
canvasElement.setAttribute("width", textSize.width + 30 + "px");
context.font = "20px Arial";
context.fillText(binding.value, 15, 20);
}
}
},
props: {
text: {
type: String,
required: true
}
}
};
</script>
<style>
.canvasText {
margin-bottom: -8px;
}
.canvasText {
margin-bottom: -8px;
}
</style>

View File

@ -1,79 +1,82 @@
<template>
<span id="forkongithub">
<a v-bind:href="url">🥄Fork me on GitHub</a>
</span>
<span id="forkongithub">
<a :href="url">🥄Fork me on GitHub</a>
</span>
</template>
<script>
export default {
name: "ForkMe",
props: {
url: String
name: "ForkMe",
props: {
url: {
type: String,
required: true
}
}
}
};
</script>
>
<style>
#forkongithub a {
background: #000;
color: #fff;
text-decoration: none;
font-family: arial, sans-serif;
text-align: center;
font-weight: bold;
padding: 5px 40px;
font-size: 1rem;
line-height: 2rem;
position: relative;
transition: 0.5s;
background: #000;
color: #fff;
text-decoration: none;
font-family: arial, sans-serif;
text-align: center;
font-weight: bold;
padding: 5px 40px;
font-size: 1rem;
line-height: 2rem;
position: relative;
transition: 0.5s;
}
#forkongithub a:hover {
background: #c11;
color: #fff;
background: #c11;
color: #fff;
}
#forkongithub a::before,
#forkongithub a::after {
content: "";
width: 100%;
display: block;
position: absolute;
top: 1px;
left: 0;
height: 1px;
background: #fff;
content: "";
width: 100%;
display: block;
position: absolute;
top: 1px;
left: 0;
height: 1px;
background: #fff;
}
#forkongithub a::after {
bottom: 1px;
top: auto;
bottom: 1px;
top: auto;
}
@media screen and (min-width:800px) {
#forkongithub {
position: absolute;
display: block;
top: 0;
right: 0;
width: 200px;
overflow: hidden;
height: 200px;
z-index: 9999;
}
@media screen and (min-width: 800px) {
#forkongithub {
position: absolute;
display: block;
top: 0;
right: 0;
width: 200px;
overflow: hidden;
height: 200px;
z-index: 9999;
}
#forkongithub a {
width: 200px;
position: absolute;
top: 60px;
right: -60px;
transform: rotate(45deg);
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-o-transform: rotate(45deg);
box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.8);
}
#forkongithub a {
width: 200px;
position: absolute;
top: 60px;
right: -60px;
transform: rotate(45deg);
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-o-transform: rotate(45deg);
box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.8);
}
}
</style>
</style>

View File

@ -1,7 +1,9 @@
<template>
<div class="fold">
<h1 class="main-title">Banana split</h1>
<input id="fold-header" type="checkbox" name="fold" v-model="unfolded" />
<h1 class="main-title">
Banana split
</h1>
<input id="fold-header" v-model="unfolded" type="checkbox" name="fold" />
<label for="fold-header">
<h2 v-if="unfolded">Shamir Secret Sharing for people with friends</h2>
<p v-else>What is this?</p>
@ -9,9 +11,9 @@
<div class="fold-content">
<p>
Banana Split uses
<a href="https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing"
>Shamir's secret sharing</a
>
<a href="https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing">
Shamir's secret sharing
</a>
to make your paper backups more resilient and secure.
</p>
<p>

View File

@ -1,31 +1,40 @@
<script>
import Vue from 'vue'
import Vue from "vue";
import ShardQrCode from "./ShardQrCode";
export default {
name: 'ShardInfo',
props: {
title: String,
shard: String,
requiredShards: Number
name: "ShardInfo",
components: { ShardQrCode },
props: {
title: {
type: String,
required: true
},
components: {ShardQrCode},
render: function () {
var element = document.createElement('div');
document.body.appendChild(element);
var passedProps = this.$props;
this.vm = new Vue({
el: element,
render: function(h) {
return h(ShardQrCode, {props: passedProps})
}
});
return this.vm;
shard: {
type: String,
required: true
},
beforeDestroy: function() {
this.vm.$el.remove();
this.vm.$destroy();
requiredShards: {
type: Number,
required: true
}
}
</script>
},
beforeDestroy: function() {
this.vm.$el.remove();
this.vm.$destroy();
},
render: function() {
var element = document.createElement("div");
document.body.appendChild(element);
var passedProps = this.$props;
this.vm = new Vue({
el: element,
render: function(h) {
return h(ShardQrCode, { props: passedProps });
}
});
return this.vm;
}
};
</script>

View File

@ -7,15 +7,13 @@
to reconstruct the secret
</h3>
</div>
<qriously
class="qr-code print-only"
v-bind:value="shard"
v-bind:size="600"
/>
<qriously class="screen-only" v-bind:value="shard" v-bind:size="200" />
<qriously class="qr-code print-only" :value="shard" :size="600" />
<qriously class="screen-only" :value="shard" :size="200" />
<div class="print-only">
<div class="recovery-field">
<div class="recovery-title">Recovery&nbsp;passphrase&nbsp;is&nbsp;</div>
<div class="recovery-title">
Recovery&nbsp;passphrase&nbsp;is&nbsp;
</div>
<div class="recovery-blank" />
</div>
<p class="version">
@ -33,9 +31,18 @@ import { version } from "../../package.json";
export default {
name: "ShardQrCode",
props: {
title: String,
shard: String,
requiredShards: Number
title: {
type: String,
required: true
},
shard: {
type: String,
required: true
},
requiredShards: {
type: Number,
required: true
}
},
computed: {
pluralizeCode: function() {

View File

@ -1,22 +1,24 @@
import Vue from 'vue'
import Vue from "vue";
import VueQriously from 'vue-qriously'
Vue.use(VueQriously)
import VueQriously from "vue-qriously";
Vue.use(VueQriously);
import QrcodeStream from "vue-qrcode-reader"
Vue.use(QrcodeStream)
import QrcodeStream from "vue-qrcode-reader";
Vue.use(QrcodeStream);
import App from './App.vue'
import router from './router'
import App from "./App.vue";
import router from "./router";
import OnlinePlugin from './plugins/online'
Vue.use(OnlinePlugin)
import OnlinePlugin from "./plugins/online";
Vue.use(OnlinePlugin);
Vue.config.productionTip = false
Vue.config.productionTip = false;
Vue.prototype.$eventHub = new Vue()
Vue.prototype.$eventHub = new Vue();
new Vue({
router,
render: function (h) { return h(App) }
}).$mount('#app')
render: function(h) {
return h(App);
}
}).$mount("#app");

View File

@ -1,26 +1,26 @@
const plugin = {
install (Vue) {
const vm = new Vue({
data: {
online: window.navigator.onLine
install(Vue) {
const vm = new Vue({
data: {
online: window.navigator.onLine
}
});
window.addEventListener("online", function handleOnline() {
vm.$data.online = true;
});
window.addEventListener("offline", function handleOffline() {
vm.$data.online = false;
});
Vue.mixin({
computed: {
isOnline() {
return vm.$data.online;
}
});
window.addEventListener('online', function handleOnline() {
vm.$data.online = true;
});
window.addEventListener('offline', function handleOffline() {
vm.$data.online = false;
});
Vue.mixin({
computed: {
isOnline() {
return vm.$data.online;
}
}
});
}
};
export default plugin
}
});
}
};
export default plugin;

View File

@ -1,28 +1,28 @@
import Vue from 'vue'
import Router from 'vue-router'
import Vue from "vue";
import Router from "vue-router";
import Info from './views/Info'
import Share from './views/Share'
import Combine from './views/Combine'
import Info from "./views/Info";
import Share from "./views/Share";
import Combine from "./views/Combine";
Vue.use(Router)
Vue.use(Router);
export default new Router({
routes: [
{
path: '/',
name: 'info',
path: "/",
name: "info",
component: Info
},
{
path: '/share',
name: 'share',
path: "/share",
name: "share",
component: Share
},
{
path: '/combine',
name: 'combine',
path: "/combine",
name: "combine",
component: Combine
}
]
})
});

File diff suppressed because it is too large Load Diff

View File

@ -2,138 +2,167 @@ const BASE64 = require("base64-js");
const CRYPTO = require("tweetnacl");
const SCRYPT = require("scryptsy");
const SECRETS = require('secrets.js-grempe');
const SECRETS = require("secrets.js-grempe");
const HexEncodeArray = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"a",
"b",
"c",
"d",
"e",
"f"
];
function strToUint8Array(str) {
return new TextEncoder("utf-8").encode(str);
return new TextEncoder("utf-8").encode(str);
}
function uint8ArrayToStr(arr) {
return new TextDecoder("utf-8").decode(arr);
return new TextDecoder("utf-8").decode(arr);
}
function hashString(str) {
return CRYPTO.hash(strToUint8Array(str));
return CRYPTO.hash(strToUint8Array(str));
}
function hexify(arr) {
var s = '';
for (var i = 0; i < arr.length; i++) {
var code = arr[i];
s += HexEncodeArray[code >>> 4];
s += HexEncodeArray[code & 0x0F];
}
return s;
var s = "";
for (var i = 0; i < arr.length; i++) {
// `i` is a numerical counter for the loop and is never changed outside of there
// therefore `i` is numerical, and is safe to use as an array index
// eslint-disable-next-line security/detect-object-injection
var code = arr[i];
s += HexEncodeArray[code >>> 4];
s += HexEncodeArray[code & 0x0f];
}
return s;
}
function dehexify(str) {
var arr = new Uint8Array(str.length / 2);
for (var i = 0; i < arr.length; i++) {
arr[i] = parseInt(str.slice(2 * i, 2 * i + 2), 16);
}
return arr;
var arr = new Uint8Array(str.length / 2);
for (var i = 0; i < arr.length; i++) {
// `i` is a numerical counter for the loop and is never changed outside of there
// therefore `i` is numerical, and is safe to use as an array index
// eslint-disable-next-line security/detect-object-injection
arr[i] = parseInt(str.slice(2 * i, 2 * i + 2), 16);
}
return arr;
}
function encrypt(data, salt, passphrase) {
var key = SCRYPT(passphrase, Buffer.from(salt), 1 << 15, 8, 1, 32);
var nonce = CRYPTO.randomBytes(24);
return {
nonce,
salt,
value: CRYPTO.secretbox(strToUint8Array(data), nonce, key)
}
var key = SCRYPT(passphrase, Buffer.from(salt), 1 << 15, 8, 1, 32);
var nonce = CRYPTO.randomBytes(24);
return {
nonce,
salt,
value: CRYPTO.secretbox(strToUint8Array(data), nonce, key)
};
}
function decrypt(data, salt, passphrase, nonce) {
var key = SCRYPT(passphrase, Buffer.from(salt.buffer), 1 << 15, 8, 1, 32);
return CRYPTO.secretbox.open(data, nonce, key);
var key = SCRYPT(passphrase, Buffer.from(salt.buffer), 1 << 15, 8, 1, 32);
// This is a false positive, `secretbox.open` is unrelated to `fs.open`
// eslint-disable-next-line security/detect-non-literal-fs-filename
return CRYPTO.secretbox.open(data, nonce, key);
}
function share(data, title, passphrase, totalShards, requiredShards) {
var salt = hashString(title);
var encrypted = encrypt(data, salt, passphrase);
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)));
var salt = hashString(title);
var encrypted = encrypt(data, salt, passphrase);
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: encodedShard,
n: nonce
}).replace(/[\u007F-\uFFFF]/g, function (chr) {
return "\\u" + ("0000" + chr.charCodeAt(0).toString(16)).substr(-4)
});
return JSON.stringify({
v: 1,
t: title,
r: requiredShards,
d: encodedShard,
n: nonce
}).replace(/[\u007F-\uFFFF]/g, function(chr) {
return "\\u" + ("0000" + chr.charCodeAt(0).toString(16)).substr(-4);
});
});
}
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,
nonce: parsed.n
}
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,
nonce: parsed.n
};
}
function reconstruct(shardObjects, passphrase) {
var shardsRequirements = shardObjects.map(shard => shard.requiredShards);
if (!shardsRequirements.every(r => r===shardsRequirements[0])) {
throw "Mismatching min shards requirement among shards!"
}
if (shardObjects.length < shardsRequirements[0]) {
throw `Not enough shards, need ${shardsRequirements[0]} but only ${shardObjects.length} provided`
}
var shardsRequirements = shardObjects.map(shard => shard.requiredShards);
if (!shardsRequirements.every(r => r === shardsRequirements[0])) {
throw "Mismatching min shards requirement among shards!";
}
if (shardObjects.length < shardsRequirements[0]) {
throw `Not enough shards, need ${shardsRequirements[0]} but only ${
shardObjects.length
} provided`;
}
var nonces = shardObjects.map(shard => shard.nonce);
if (!nonces.every(n => n===nonces[0])) {
throw "Nonces mismatch among shards!"
}
var nonces = shardObjects.map(shard => shard.nonce);
if (!nonces.every(n => n === nonces[0])) {
throw "Nonces mismatch among shards!";
}
var titles = shardObjects.map((shard) => shard.title);
if (!titles.every(t => t===titles[0])) {
throw "Titles mismatch among shards!"
}
var titles = shardObjects.map(shard => shard.title);
if (!titles.every(t => t === titles[0])) {
throw "Titles mismatch among shards!";
}
var versions = shardObjects.map(shard => shard.version);
if (!versions.every(v => v===versions[0])) {
throw "Versions mismatch along shards!"
}
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));
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));
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!";
}
default:
throw "Version is not supported!";
}
}
export default {
share,
parse,
reconstruct
}
share,
parse,
reconstruct
};

View File

@ -4,38 +4,42 @@
Combine shards for <em v-if="title"> {{ title }}</em>
</h1>
<div v-if="needMoreShards">
<qrcode-stream v-on:decode="onDecode" />
<qrcode-stream @decode="onDecode" />
<div class="codes-row">
<qriously
class="qrcode"
v-for="code in qrCodes"
v-bind:key="code"
v-bind:value="code"
v-bind:size="200"
v-bind:padding="0"
:key="code"
class="qrcode"
:value="code"
:size="200"
:padding="0"
/>
<qriously
class="qrcode remaining"
v-for="n in remainingCodes"
v-bind:key="n"
value='{"t":"Very secret info","r":2,"d":"803c12929ba469d720a63fc8ca6f6ef1cc441f8f0b830ea04f8a484169ec800e4a7","n":"29abbb2c509adedb470f6d3fd3d083362b8c1a9283adf987"}'
v-bind:size="200"
:key="n"
class="qrcode remaining"
:value="PLACEHOLDER_QR_DATA"
:size="200"
foreground="#aaa"
v-bind:padding="0"
:padding="0"
/>
</div>
</div>
<div v-else>
<input
type="text"
v-model="passphrase"
v-on:keyup.enter="reconstruct"
type="text"
placeholder="type your passphrase"
autofocus
@keyup.enter="reconstruct"
/>
<button v-on:click="reconstruct">Reconstruct Secret</button>
<button @click="reconstruct">
Reconstruct Secret
</button>
</div>
<h2 class="secret" v-if="recoveredSecret">{{ recoveredSecret }}</h2>
<h2 v-if="recoveredSecret" class="secret">
{{ recoveredSecret }}
</h2>
</div>
</template>
@ -44,6 +48,11 @@
import crypto from "../util/crypto";
// It's actually used in the template
// eslint-disable-next-line no-unused-vars
const PLACEHOLDER_QR_DATA =
'{"t":"Very secret info","r":2,"d":"803c12929ba469d720a63fc8ca6f6ef1cc441f8f0b830ea04f8a484169ec800e4a7","n":"29abbb2c509adedb470f6d3fd3d083362b8c1a9283adf987"}';
export default {
name: "Combine",
data: function() {
@ -69,6 +78,9 @@ export default {
}
}
},
mounted: function() {
this.$eventHub.$emit("foldGeneralInfo");
},
methods: {
onDecode: function(result) {
var parsed = crypto.parse(result);
@ -111,9 +123,6 @@ export default {
var shards = Array.from(this.shards);
this.recoveredSecret = crypto.reconstruct(shards, this.passphrase);
}
},
mounted: function() {
this.$eventHub.$emit("foldGeneralInfo");
}
};
</script>

View File

@ -1,14 +1,9 @@
<template>
<div>
</div>
<div />
</template>
<script>
export default {
}
export default {};
</script>
<style>
</style>
<style></style>

View File

@ -5,48 +5,52 @@
<p>
Name of your split
<input
v-model="title"
type="text"
:disabled="encryptionMode"
v-model="title"
placeholder="Ex: 'My Bitcoin seed phrase'"
autofocus
/>
</p>
<textarea
v-model="secret"
v-bind:class="{ tooLong: secretTooLong }"
:class="{ tooLong: secretTooLong }"
:disabled="encryptionMode"
placeholder="Your secret goes here"
></textarea>
<div v-if="this.secretTooLong">
/>
<div v-if="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
<input v-model.number="totalShards" type="number" min="3" /> to
reconstruct
</p>
<button :disabled="secretTooLong" v-on:click="toggleMode">
<span v-if="this.encryptionMode">Back to editing data</span>
<span v-if="encryptionMode">Back to editing data</span>
<span v-else>Generate QR codes!</span>
</button>
<div v-if="this.encryptionMode">
<div v-if="encryptionMode">
<p>Your passphrase for the recovery is:</p>
<p>
<canvas-text v-bind:text="recoveryPassphrase" />
<button v-on:click="regenPassphrase">&#x21ba;</button>
<canvas-text :text="recoveryPassphrase" />
<button @click="regenPassphrase">
&#x21ba;
</button>
</p>
<button v-on:click="print">Print us!</button>
<button @click="print">
Print us!
</button>
</div>
</div>
<div id="qr-tiles">
<shard-info
v-for="shard in shards"
v-bind:key="shard"
v-bind:shard="shard"
v-bind:requiredShards="requiredShards"
v-bind:title="title"
:key="shard"
:shard="shard"
:required-shards="requiredShards"
:title="title"
/>
</div>
</div>
@ -61,6 +65,7 @@ import CanvasText from "../components/CanvasText";
export default {
name: "Share",
components: { ShardInfo, CanvasText },
data: function() {
return {
title: "",
@ -70,7 +75,6 @@ export default {
encryptionMode: false
};
},
components: { ShardInfo, CanvasText },
computed: {
secretTooLong: function() {
return this.secret.length > 1024;
@ -91,6 +95,9 @@ export default {
);
}
},
mounted: function() {
this.$eventHub.$emit("foldGeneralInfo");
},
methods: {
regenPassphrase: function() {
this.recoveryPassphrase = bipPhrase.generate(4);
@ -101,9 +108,6 @@ export default {
toggleMode: function() {
this.encryptionMode = !this.encryptionMode;
}
},
mounted: function() {
this.$eventHub.$emit("foldGeneralInfo");
}
};
</script>

View File

@ -1,98 +1,134 @@
require('fast-text-encoding'); // NodeJS requires this polyfill
/* eslint-disable security/detect-object-injection */
require("fast-text-encoding"); // NodeJS requires this polyfill
import crypto from "../../src/util/crypto";
test('encodes-decodes empty values', () => {
var shards = crypto.share('', '', '', 3, 2);
var parsed = shards.map(s => crypto.parse(s));
var reconstructed = crypto.reconstruct(parsed, '');
expect(reconstructed).toBe('');
})
test("encodes-decodes empty values", () => {
var shards = crypto.share("", "", "", 3, 2);
var parsed = shards.map(s => crypto.parse(s));
var reconstructed = crypto.reconstruct(parsed, "");
expect(reconstructed).toBe("");
});
test('properly deserializes metadata', () => {
var shards = crypto.share('Secret message', 'Secret title', 'correct-horse-battery-staple', 5, 3);
var parsed = shards.map(s => crypto.parse(s));
parsed.forEach(message => {
expect(message.title).toBe('Secret title');
expect(message.requiredShards).toBe(3);
expect(message.nonce).toBe(parsed[0].nonce);
});
})
test("properly deserializes metadata", () => {
var shards = crypto.share(
"Secret message",
"Secret title",
"correct-horse-battery-staple",
5,
3
);
var parsed = shards.map(s => crypto.parse(s));
parsed.forEach(message => {
expect(message.title).toBe("Secret title");
expect(message.requiredShards).toBe(3);
expect(message.nonce).toBe(parsed[0].nonce);
});
});
test('works with any 2 shards out of 3', () => {
var shards = crypto.share('Secret message', 'Secret title', 'correct-horse-battery-staple', 3, 2);
var parsed = shards.map(s => crypto.parse(s));
[
test("works with any 2 shards out of 3", () => {
var shards = crypto.share(
"Secret message",
"Secret title",
"correct-horse-battery-staple",
3,
2
);
var parsed = shards.map(s => crypto.parse(s));
[[0, 1], [0, 2], [1, 2], [1, 0], [2, 0], [2, 1]].forEach(([i, j]) => {
expect(
crypto.reconstruct([parsed[i], parsed[j]], "correct-horse-battery-staple")
).toBe("Secret message");
});
});
[0, 1], [0, 2], [1, 2],
[1, 0], [2, 0], [2, 1]
].forEach(([i, j]) => {
expect(crypto.reconstruct([parsed[i], parsed[j]], 'correct-horse-battery-staple')).toBe('Secret message')
});
})
test("fails with incorrect password", () => {
var shards = crypto.share(
"Secret message",
"Secret title",
"correct-horse-battery-staple",
5,
3
);
var parsed = shards.map(s => crypto.parse(s));
var reconstructed = crypto.reconstruct(parsed, "");
expect(reconstructed).not.toBe("Secret message");
expect(reconstructed).toBe("");
});
test('fails with incorrect password', () => {
var shards = crypto.share('Secret message', 'Secret title', 'correct-horse-battery-staple', 5, 3);
var parsed = shards.map(s => crypto.parse(s));
var reconstructed = crypto.reconstruct(parsed, '');
expect(reconstructed).not.toBe('Secret message');
expect(reconstructed).toBe('');
})
test("works with unicode strings", () => {
var shards = crypto.share(
"Текст сообщения",
"Это секрет",
"correct-horse-battery-staple",
5,
3
);
var parsed = shards.map(s => crypto.parse(s));
parsed.forEach(message => {
expect(message.title).toBe("Это секрет");
});
test('works with unicode strings', () => {
var shards = crypto.share('Текст сообщения', 'Это секрет', 'correct-horse-battery-staple', 5, 3);
var parsed = shards.map(s => crypto.parse(s));
parsed.forEach(message => {
expect(message.title).toBe('Это секрет')
});
var reconstructed = crypto.reconstruct(
parsed,
"correct-horse-battery-staple"
);
expect(reconstructed).toBe("Текст сообщения");
});
var reconstructed = crypto.reconstruct(parsed, 'correct-horse-battery-staple');
expect(reconstructed).toBe('Текст сообщения');
})
test("reconstructs the reference example of v0 serialization", () => {
var shards = [
'{"t":"Very secret info","r":2,"d":"803c12929ba469d720a63fc8ca6f6ef1cc441f8f0b830ea04f8a484169ec800e4a7","n":"29abbb2c509adedb470f6d3fd3d083362b8c1a9283adf987"}',
'{"t":"Very secret info","r":2,"d":"801b4196813bb664141169f939414fec30c7f343326d8df45e557b25b0249fd43e2","n":"29abbb2c509adedb470f6d3fd3d083362b8c1a9283adf987"}',
'{"t":"Very secret info","r":2,"d":"80275318760b66ee5a1d7430dbf8769fda05e9e1ff7447eaa78539fbed006f0390b","n":"29abbb2c509adedb470f6d3fd3d083362b8c1a9283adf987"}'
].map(s => crypto.parse(s));
test('reconstructs the reference example of v0 serialization', () => {
var shards = [
'{"t":"Very secret info","r":2,"d":"803c12929ba469d720a63fc8ca6f6ef1cc441f8f0b830ea04f8a484169ec800e4a7","n":"29abbb2c509adedb470f6d3fd3d083362b8c1a9283adf987"}',
'{"t":"Very secret info","r":2,"d":"801b4196813bb664141169f939414fec30c7f343326d8df45e557b25b0249fd43e2","n":"29abbb2c509adedb470f6d3fd3d083362b8c1a9283adf987"}',
'{"t":"Very secret info","r":2,"d":"80275318760b66ee5a1d7430dbf8769fda05e9e1ff7447eaa78539fbed006f0390b","n":"29abbb2c509adedb470f6d3fd3d083362b8c1a9283adf987"}'
].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]], "amazing-daring-panda-horror")
).toBe("Any questions?");
});
});
[
[0, 1], [0, 2], [1, 2],
[1, 0], [2, 0], [2, 1]
].forEach(([i,j]) => {
expect(crypto.reconstruct([shards[i], shards[j]], 'amazing-daring-panda-horror')).toBe('Any questions?')
});
})
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));
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");
});
});
[
[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";
expect(() =>
crypto.reconstruct([shards[0], shards[1]], "correct-horse-battery-staple")
).toThrow(/Nonces mismatch/);
});
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"
expect(() => crypto.reconstruct([shards[0], shards[1]], 'correct-horse-battery-staple')).toThrow(/Nonces mismatch/)
})
test('throws an error if titles mismatch', () => {
var shards = crypto.share('Message', 'Title', 'correct-horse-battery-staple', 3, 2).map(s => crypto.parse(s));
shards[0].title = "Completely different title"
expect(() => crypto.reconstruct([shards[0], shards[1]], 'correct-horse-battery-staple')).toThrow(/Titles mismatch/)
})
test("throws an error if titles mismatch", () => {
var shards = crypto
.share("Message", "Title", "correct-horse-battery-staple", 3, 2)
.map(s => crypto.parse(s));
shards[0].title = "Completely different title";
expect(() =>
crypto.reconstruct([shards[0], shards[1]], "correct-horse-battery-staple")
).toThrow(/Titles mismatch/);
});
test("doesn't work if less shards than necessary are supplied", () => {
var shards = crypto.share('Message', 'Title', 'correct-horse-battery-staple', 5, 3).map(s => crypto.parse(s));
expect(() => crypto.reconstruct([shards[0], shards[1]], 'correct-horse-battery-staple')).toThrow(/Not enough shards/)
})
var shards = crypto
.share("Message", "Title", "correct-horse-battery-staple", 5, 3)
.map(s => crypto.parse(s));
expect(() =>
crypto.reconstruct([shards[0], shards[1]], "correct-horse-battery-staple")
).toThrow(/Not enough shards/);
});

View File

@ -1,9 +1,10 @@
let HtmlWebpackPlugin = require('html-webpack-plugin');
let HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
let Webpack = require('webpack');
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();
// eslint-disable-next-line security/detect-child-process
let childProcess = require("child_process");
let GIT_REVISION = childProcess.execSync("git rev-parse HEAD").toString();
module.exports = {
productionSourceMap: false,
@ -13,15 +14,15 @@ module.exports = {
configureWebpack: {
plugins: [
new HtmlWebpackPlugin({
template: 'public/index.html',
inlineSource: '.(js|css)$'
template: "public/index.html",
inlineSource: ".(js|css)$"
}),
new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin),
new Webpack.DefinePlugin({
'process.env': {
'GIT_REVISION': JSON.stringify(GIT_REVISION)
"process.env": {
GIT_REVISION: JSON.stringify(GIT_REVISION)
}
})
]
}
}
};