Add pre-versioning work

This commit is contained in:
Andrew Morris
2023-05-22 13:05:20 +10:00
commit 4c122ae1e8
7 changed files with 2459 additions and 0 deletions

9
.vscode/settings.json vendored Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

62
manualTest.ts Normal file
View 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
View 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));
}