mirror of
https://github.com/voltrevo/seed-splitter.git
synced 2026-01-10 05:28:10 -05:00
Add pre-versioning work
This commit is contained in:
9
.vscode/settings.json
vendored
Normal file
9
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"deno.enable": true,
|
||||
"deno.unstable": true,
|
||||
"deno.lint": true,
|
||||
"deno.config": "./.vscode/deno.jsonc",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "denoland.vscode-deno",
|
||||
"editor.rulers": [80]
|
||||
}
|
||||
167
FieldElement.ts
Normal file
167
FieldElement.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import bip39WordList from "./bip39Wordlist.ts";
|
||||
|
||||
const P = 2n ** 128n - 159n; // The largest prime below 2**128
|
||||
|
||||
class FieldElement {
|
||||
constructor(public n: bigint) {}
|
||||
|
||||
add(other: FieldElement): FieldElement {
|
||||
return new FieldElement((this.n + other.n) % P);
|
||||
}
|
||||
|
||||
sub(other: FieldElement): FieldElement {
|
||||
return new FieldElement((this.n - other.n + P) % P);
|
||||
}
|
||||
|
||||
mul(other: FieldElement): FieldElement {
|
||||
return new FieldElement((this.n * other.n) % P);
|
||||
}
|
||||
|
||||
negate(): FieldElement {
|
||||
return new FieldElement((-this.n + P) % P);
|
||||
}
|
||||
|
||||
pow(exponent: bigint): FieldElement {
|
||||
let result = new FieldElement(1n);
|
||||
|
||||
// deno-lint-ignore no-this-alias
|
||||
let base: FieldElement = this;
|
||||
|
||||
while (exponent > 0n) {
|
||||
if (exponent & 1n) {
|
||||
result = result.mul(base);
|
||||
}
|
||||
|
||||
base = base.mul(base);
|
||||
exponent >>= 1n;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
inverse(): FieldElement {
|
||||
return this.pow(P - 2n);
|
||||
}
|
||||
|
||||
static random(): FieldElement {
|
||||
const buf = crypto.getRandomValues(new Uint8Array(16));
|
||||
return FieldElement.fromBuffer(buf);
|
||||
}
|
||||
|
||||
static fromBuffer(buf: Uint8Array) {
|
||||
if (buf.length !== 16) {
|
||||
throw new Error("Buffer should be 16 bytes");
|
||||
}
|
||||
|
||||
let n = 0n;
|
||||
|
||||
for (let i = 0; i < 16; i++) {
|
||||
n *= 256n;
|
||||
n += BigInt(buf[i]);
|
||||
}
|
||||
|
||||
return new FieldElement(n);
|
||||
}
|
||||
|
||||
toBuffer(): Uint8Array {
|
||||
let x = this.n;
|
||||
|
||||
const buf = new Uint8Array(16);
|
||||
|
||||
for (let i = 15; i >= 0; i--) {
|
||||
buf[i] = Number(x % 256n);
|
||||
x /= 256n;
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
async toMnemonic(): Promise<string[]> {
|
||||
const buf = this.toBuffer();
|
||||
|
||||
const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", buf));
|
||||
|
||||
let x = this.n;
|
||||
|
||||
const wordNums: number[] = [
|
||||
(Number(x % 128n) << 4) + (hash[0] >> 4),
|
||||
];
|
||||
|
||||
x /= 128n;
|
||||
|
||||
for (let i = 0; i < 11; i++) {
|
||||
wordNums.unshift(Number(x % 2048n));
|
||||
x /= 2048n;
|
||||
}
|
||||
|
||||
return wordNums.map((n) => bip39WordList[n]);
|
||||
}
|
||||
|
||||
static async fromMnemonic(words: string[]) {
|
||||
if (words.length !== 12) {
|
||||
throw new Error("Only 12-word mnemonics are supported");
|
||||
}
|
||||
|
||||
let x = 0n;
|
||||
|
||||
for (const word of words) {
|
||||
x *= 2048n;
|
||||
x += BigInt(bip39WordList.indexOf(word));
|
||||
}
|
||||
|
||||
const checksum = Number(x % 16n);
|
||||
x /= 16n;
|
||||
|
||||
const fieldElement = new FieldElement(x);
|
||||
|
||||
const hash = new Uint8Array(
|
||||
await crypto.subtle.digest("SHA-256", fieldElement.toBuffer()),
|
||||
);
|
||||
|
||||
if (checksum !== (hash[0] >> 4)) {
|
||||
throw new Error("Checksum doesn't match");
|
||||
}
|
||||
|
||||
return fieldElement;
|
||||
}
|
||||
|
||||
toLabel() {
|
||||
let label = "";
|
||||
|
||||
let x = this.n;
|
||||
|
||||
while (x > 0n) {
|
||||
const cNum = x % 27n;
|
||||
x /= 27n;
|
||||
|
||||
const c = cNum === 0n ? "-" : String.fromCodePoint(96 + Number(cNum));
|
||||
label = c + label;
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
static fromLabel(label: string) {
|
||||
let x = 0n;
|
||||
|
||||
for (const c of label) {
|
||||
x *= 27n;
|
||||
|
||||
if (["-", "_", " "].includes(c)) {
|
||||
x += 0n;
|
||||
} else {
|
||||
const code = (c.toLowerCase().codePointAt(0) ?? 0) - 96;
|
||||
|
||||
if (code < 0 || code >= 27) {
|
||||
throw new Error(`Invalid code point ${c}`);
|
||||
}
|
||||
|
||||
x += BigInt(code);
|
||||
}
|
||||
}
|
||||
|
||||
return new FieldElement(x);
|
||||
}
|
||||
}
|
||||
|
||||
export default FieldElement;
|
||||
53
Polynomial.ts
Normal file
53
Polynomial.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import FieldElement from "./FieldElement.ts";
|
||||
|
||||
export default class Polynomial {
|
||||
constructor(public coeffs: FieldElement[]) {}
|
||||
|
||||
add(other: Polynomial) {
|
||||
const res = new Polynomial(new Array(
|
||||
Math.max(this.coeffs.length, other.coeffs.length),
|
||||
).fill(new FieldElement(0n)));
|
||||
|
||||
for (const poly of [this, other]) {
|
||||
for (const [i, coeff] of poly.coeffs.entries()) {
|
||||
res.coeffs[i] = res.coeffs[i].add(coeff);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
mul(other: Polynomial) {
|
||||
if (this.coeffs.length === 0 || other.coeffs.length === 0) {
|
||||
return new Polynomial([]);
|
||||
}
|
||||
|
||||
const res = new Polynomial(new Array(
|
||||
this.coeffs.length + other.coeffs.length - 1,
|
||||
).fill(new FieldElement(0n)));
|
||||
|
||||
for (const [i, ai] of this.coeffs.entries()) {
|
||||
for (const [j, bj] of other.coeffs.entries()) {
|
||||
res.coeffs[i + j] = res.coeffs[i + j].add(ai.mul(bj));
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
eval(x: FieldElement) {
|
||||
if (this.coeffs.length === 0) {
|
||||
return new FieldElement(0n);
|
||||
}
|
||||
|
||||
let sum = new FieldElement(0n);
|
||||
let xPow = new FieldElement(1n);
|
||||
|
||||
for (const coeff of this.coeffs) {
|
||||
sum = sum.add(coeff.mul(xPow));
|
||||
xPow = xPow.mul(x);
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
63
RecoveryCurve.ts
Normal file
63
RecoveryCurve.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import FieldElement from "./FieldElement.ts";
|
||||
import Polynomial from "./Polynomial.ts";
|
||||
|
||||
type Point = {
|
||||
label: string;
|
||||
mnemonic: string[];
|
||||
};
|
||||
|
||||
export default class RecoveryCurve {
|
||||
private constructor(public poly: Polynomial) {}
|
||||
|
||||
static async fit(points: Point[]) {
|
||||
const rawPoints = await Promise.all(
|
||||
points.map(async ({ label, mnemonic }) => ({
|
||||
x: FieldElement.fromLabel(label),
|
||||
y: await FieldElement.fromMnemonic(mnemonic),
|
||||
})),
|
||||
);
|
||||
|
||||
let sum = new Polynomial([]);
|
||||
|
||||
for (const share of rawPoints) {
|
||||
let poly = new Polynomial([new FieldElement(1n)]);
|
||||
|
||||
for (const otherShare of rawPoints) {
|
||||
if (otherShare === share) {
|
||||
continue;
|
||||
}
|
||||
|
||||
poly = poly.mul(
|
||||
new Polynomial([
|
||||
otherShare.x,
|
||||
new FieldElement(1n).negate(),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
poly = poly.mul(
|
||||
new Polynomial([
|
||||
poly.eval(share.x).inverse().mul(share.y),
|
||||
]),
|
||||
);
|
||||
|
||||
sum = sum.add(poly);
|
||||
}
|
||||
|
||||
return new RecoveryCurve(sum);
|
||||
}
|
||||
|
||||
async eval(label: string) {
|
||||
const x = FieldElement.fromLabel(label);
|
||||
const y = this.poly.eval(x);
|
||||
|
||||
return await y.toMnemonic();
|
||||
}
|
||||
|
||||
static async randomPoint(): Promise<Point> {
|
||||
return {
|
||||
label: FieldElement.random().toLabel(),
|
||||
mnemonic: await FieldElement.random().toMnemonic(),
|
||||
};
|
||||
}
|
||||
}
|
||||
2052
bip39WordList.ts
Normal file
2052
bip39WordList.ts
Normal file
File diff suppressed because it is too large
Load Diff
62
manualTest.ts
Normal file
62
manualTest.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// import * as shamir from "./shamir.ts";
|
||||
// import FieldElement from "./FieldElement.ts";
|
||||
import RecoveryCurve from "./RecoveryCurve.ts";
|
||||
|
||||
// const shares = shamir.generate(new FieldElement(123n), 2, 3);
|
||||
|
||||
// console.log(
|
||||
// await Promise.all(
|
||||
// shares.map(async (s) =>
|
||||
// `${Number(s.x.n)}: ${(await s.y.toMnemonic()).join(" ")}`
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
|
||||
// const recovered = shamir.recover([shares[0], shares[2]]);
|
||||
|
||||
// console.log((await recovered.toMnemonic()).join(" "));
|
||||
|
||||
// const secret =
|
||||
// "expire gun route tornado female reflect holiday grief spring clown deliver army";
|
||||
|
||||
// const curve = await RecoveryCurve.fit([
|
||||
// {
|
||||
// label: "secret",
|
||||
// mnemonic: secret.split(" "),
|
||||
// },
|
||||
// await RecoveryCurve.randomPoint(),
|
||||
// await RecoveryCurve.randomPoint(),
|
||||
// ]);
|
||||
|
||||
const recoveryData = `
|
||||
charlotte: valley when dial juice vapor ill payment lunch brass zone alpha topic
|
||||
daniel: long neutral reject ahead cart proud shoulder auction april same solar match
|
||||
emily: junior coach innocent fragile aware ritual recipe rhythm equal vicious path tray
|
||||
`;
|
||||
|
||||
const curve = await RecoveryCurve.fit(
|
||||
recoveryData.split("\n").map((line) => {
|
||||
if (line.trim() === "") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const [label, mnemonic] = line.split(": ");
|
||||
|
||||
return { label, mnemonic: mnemonic.split(" ") };
|
||||
}).flat(),
|
||||
);
|
||||
|
||||
const names = [
|
||||
"secret",
|
||||
"ava",
|
||||
"benjamin",
|
||||
"charlotte",
|
||||
"daniel",
|
||||
"emily",
|
||||
"finn",
|
||||
"grace",
|
||||
];
|
||||
|
||||
for (const name of names) {
|
||||
console.log(`${name}:`, (await curve.eval(name)).join(" "));
|
||||
}
|
||||
53
shamir.ts
Normal file
53
shamir.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import FieldElement from "./FieldElement.ts";
|
||||
import Polynomial from "./Polynomial.ts";
|
||||
|
||||
type Point = { x: FieldElement; y: FieldElement };
|
||||
|
||||
export function generate(
|
||||
secret: FieldElement,
|
||||
k: number,
|
||||
n: number,
|
||||
): Point[] {
|
||||
const poly = new Polynomial([
|
||||
secret,
|
||||
...[...new Array(k - 1)].map(() => FieldElement.random()),
|
||||
]);
|
||||
|
||||
const shares = [...new Array(n)].map((_, i) => {
|
||||
const x = new FieldElement(BigInt(i) + 1n);
|
||||
return { x, y: poly.eval(x) };
|
||||
});
|
||||
|
||||
return shares;
|
||||
}
|
||||
|
||||
export function recover(shares: Point[]) {
|
||||
let sum = new Polynomial([]);
|
||||
|
||||
for (const share of shares) {
|
||||
let poly = new Polynomial([new FieldElement(1n)]);
|
||||
|
||||
for (const otherShare of shares) {
|
||||
if (otherShare === share) {
|
||||
continue;
|
||||
}
|
||||
|
||||
poly = poly.mul(
|
||||
new Polynomial([
|
||||
otherShare.x,
|
||||
new FieldElement(1n).negate(),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
poly = poly.mul(
|
||||
new Polynomial([
|
||||
poly.eval(share.x).inverse().mul(share.y),
|
||||
]),
|
||||
);
|
||||
|
||||
sum = sum.add(poly);
|
||||
}
|
||||
|
||||
return sum.eval(new FieldElement(0n));
|
||||
}
|
||||
Reference in New Issue
Block a user