Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/util/shards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Banana Split's fixed threshold policy: reconstruction requires a majority of
// the shards, i.e. floor(total / 2) + 1. This lives in one place so the
// generator (Share.vue) and the reprint view (Print.vue) can never disagree —
// they did once: Print.vue re-implemented the formula as floor(total/2)+2 and
// displayed the wrong "you need N more" count on reprints (finding F5).
export function defaultThreshold(totalShards: number): number {
return Math.floor(totalShards / 2) + 1;
}
20 changes: 12 additions & 8 deletions src/views/Print.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<br>
<input
id="totalShards"
v-model.number="requiredShards"
v-model.number="totalShards"
type="number"
min="3"
max="255"
Comment on lines 13 to 17
Expand All @@ -36,7 +36,7 @@
v-for="code in qrCodes"
:key="code"
:shard="code"
:required-shards="parseInt(requiredShards/2)+2"
:required-shards="threshold"
:title="title"
/>
</div>
Expand Down Expand Up @@ -64,6 +64,7 @@

<script lang="ts">
import crypto, { Shard } from "../util/crypto";
import { defaultThreshold } from "../util/shards";
import ShardInfo from "../components/ShardInfo.vue";

import Vue from "vue";
Expand All @@ -73,7 +74,7 @@ type PrintData = {
nonce: string;
shards: Shard[];
qrCodes: Set<string>;
requiredShards?: number;
totalShards?: number;
numberEntered: boolean;
PLACEHOLDER_QR_DATA: string;
};
Expand All @@ -87,21 +88,24 @@ export default Vue.extend({
nonce: "",
shards: [],
qrCodes: new Set(),
requiredShards: undefined,
totalShards: undefined,
numberEntered: false,
PLACEHOLDER_QR_DATA: ""
};
},
computed: {
needMoreShards(): boolean {
return this.requiredShards !== undefined && this.shards.length !== this.requiredShards;
return this.totalShards !== undefined && this.shards.length !== this.totalShards;
},
remainingCodes(): number {
if (!this.requiredShards) {
if (!this.totalShards) {
return 0;
} else {
return this.requiredShards - this.shards.length;
return this.totalShards - this.shards.length;
}
Comment on lines +98 to 105
},
threshold(): number {
return this.totalShards ? defaultThreshold(this.totalShards) : 0;
Comment on lines +107 to +108
}
},
mounted: function() {
Expand Down Expand Up @@ -144,7 +148,7 @@ export default Vue.extend({
window.print();
},
handleShardsInput: function() {
if (this.requiredShards && this.requiredShards >= 3 && this.requiredShards <= 255) {
if (this.totalShards && this.totalShards >= 3 && this.totalShards <= 255) {
this.numberEntered = true;
} else {
this.$eventHub.$emit("showError", "Please enter a valid number of shards between 3 and 255.");
Expand Down
3 changes: 2 additions & 1 deletion src/views/Share.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
<script lang="ts">
import passPhrase from "../util/passPhrase";
import crypto from "../util/crypto";
import { defaultThreshold } from "../util/shards";

import ShardInfo from "../components/ShardInfo.vue";
import CanvasText from "../components/CanvasText.vue";
Expand Down Expand Up @@ -121,7 +122,7 @@ export default Vue.extend({
return this.secret.length > 1024;
},
requiredShards(): number {
return Math.floor(this.totalShards / 2) + 1;
return defaultThreshold(this.totalShards);
},
shards(): string[] {
this.$eventHub.$emit("clearAlerts");
Expand Down
24 changes: 24 additions & 0 deletions tests/unit/shards.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { defaultThreshold } from "../../src/util/shards";

describe("defaultThreshold", () => {
// The bug fixed in F5 was Print.vue computing floor(total/2)+2 instead of
// floor(total/2)+1 — e.g. total=5 showed "need 4" instead of the correct 3.
test.each([
[3, 2],
[4, 3],
[5, 3], // the exact case from finding F5
[6, 4],
[7, 4],
[10, 6],
[19, 10],
[255, 128]
])("threshold for %i total shards is %i", (total, expected) => {
expect(defaultThreshold(total)).toBe(expected);
});

test("always requires a strict majority across the whole UI range", () => {
for (let n = 3; n <= 255; n++) {
expect(defaultThreshold(n)).toBeGreaterThan(n / 2);
}
});
});