]> git.ardis.lu - seed-phrase-transposer/commitdiff
Initial commit
authorArdis Lu <ardisglu@gmail.com>
Sun, 3 Sep 2023 07:32:29 +0000 (00:32 -0700)
committerArdis Lu <ardisglu@gmail.com>
Sun, 3 Sep 2023 07:32:29 +0000 (00:32 -0700)
.gitignore [new file with mode: 0644]
README.md [new file with mode: 0644]
main.ts [new file with mode: 0644]
main_test.ts [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..722d5e7
--- /dev/null
@@ -0,0 +1 @@
+.vscode
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..6e012f1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,50 @@
+# seed-phrase-transposer
+
+This is a simple [Deno](https://deno.land/) CLI to print possible permutations
+of a seed phrase to help test if a transposition error occurred while the seed
+phrase was recorded.
+
+## Background
+
+12-word and 24-word
+[BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) seed
+phrases are ubiquitous in crypto. However, due to the length of the seed phrase,
+they're often presented to users in a grid. Sometimes bad wallet UIs do not
+number the words,
+[making the order of the words ambiguous](https://knowyourmeme.com/memes/dont-dead-open-inside)
+and thus introducing the risk of a user recording the words in the wrong order
+(a transposition error).
+
+This tool is intended to show possible permutations of a seed phrase when read
+from ambiguous grid layouts to assist with testing and potential recovery.
+
+## Example
+
+Consider this 3x4 grid layout:
+
+```
+1  2  3
+4  5  6
+7  8  9
+10 11 12
+```
+
+The grid is intended to be read left-to-right, top-to-bottom. But a user may
+incorrectly record these numbers top-to-bottom, left-to-right. Like this:
+
+```
+1 4 7 10 2 5 8 11 3 6 9 12
+```
+
+This CLI will output possible permutations of the input, enabling a user to
+quickly discover the correct one:
+
+```
+> deno run main.ts
+Seed phrase: 1 4 7 10 2 5 8 11 3 6 9 12
+
+1 8 4 11 7 3 10 6 2 9 5 12
+1 7 2 8 3 9 4 10 5 11 6 12
+1 2 3 4 5 6 7 8 9 10 11 12 <-- Correct permutation found!
+1 10 8 6 4 2 11 9 7 5 3 12
+```
diff --git a/main.ts b/main.ts
new file mode 100644 (file)
index 0000000..15cb401
--- /dev/null
+++ b/main.ts
@@ -0,0 +1,56 @@
+export function parseInput(input: string | null) {
+  if (input === null) {
+    throw new Error("No seed phrase entered.");
+  }
+  const words = input.split(/\s/g).map((w) => w.trim());
+  if (words.length % 2 === 1) {
+    throw new Error("Seed phrase length must be even.");
+  }
+  return words;
+}
+
+export function getPermutations(words: Array<string>, columns: number) {
+  if (words.length % columns !== 0) {
+    throw new Error(
+      "Seed phrase length must be divisible by number of columns.",
+    );
+  }
+
+  const rows = words.length / columns;
+
+  // Assuming only permutations are horizontal-tb and vertical-lr
+  // Note that a 3x4 grid read sideways is the same as a 4x3 grid, so only need to check lower factors
+  const verticalLR = [];
+  const horizontalTB = [];
+  for (const [index, word] of words.entries()) {
+    const verticalIndex = (index % rows) * columns + Math.floor(index / rows);
+    verticalLR[verticalIndex] = word;
+
+    const horizontalIndex = (index % columns) * rows +
+      Math.floor(index / columns);
+    horizontalTB[horizontalIndex] = word;
+  }
+
+  return [verticalLR.join(" "), horizontalTB.join(" ")];
+}
+
+// Gets all factors that are >= 2 and not a multiple of another factor
+export function getLowerFactors(num: number) {
+  const lowerFactors = [];
+  for (let i = 2; i < num / (lowerFactors.at(-1) ?? 1); i++) {
+    if (num % i === 0) {
+      lowerFactors.push(i);
+    }
+  }
+  return lowerFactors;
+}
+
+if (import.meta.main) {
+  const input = prompt("Seed phrase:");
+  const words = parseInput(input);
+  const factors = getLowerFactors(words.length);
+  const permutations = factors.map((columns) => getPermutations(words, columns))
+    .flat();
+  console.log("\n");
+  permutations.forEach((permutation) => console.log(permutation));
+}
diff --git a/main_test.ts b/main_test.ts
new file mode 100644 (file)
index 0000000..e4267be
--- /dev/null
@@ -0,0 +1,143 @@
+import { getLowerFactors, getPermutations, parseInput } from "./main.ts";
+import {
+  assertArrayIncludes,
+  assertEquals,
+  assertFalse,
+  assertThrows,
+} from "https://deno.land/std@0.201.0/assert/mod.ts";
+
+Deno.test("parseInput throws on null", () => {
+  assertThrows(() => parseInput(null));
+});
+
+Deno.test("parseInput throws on invalid length", () => {
+  assertThrows(() => parseInput("test test test"));
+});
+
+Deno.test("parseInput parses 12 word seed phrase", () => {
+  const words = parseInput(
+    "test test test test test test test test test test test junk",
+  );
+  assertEquals(words.length, 12);
+});
+
+Deno.test("parseInput parses 24 word seed phrase", () => {
+  const words = parseInput(
+    "test test test test test test test test test test test junk test test test test test test test test test test test junk",
+  );
+  assertEquals(words.length, 24);
+});
+
+Deno.test("getPermutations throws on invalid number of columns", () => {
+  assertThrows(() =>
+    getPermutations(
+      "test test test test test test test test test test test junk".split(" "),
+      5,
+    )
+  );
+});
+
+/*
+Example: "3-column vertical-lr incorrectly recorded as horizontal-tb"
+
+Grid:
+1  2  3
+4  5  6
+7  8  9
+10 11 12
+
+User enters:
+1 4 7 10 2 5 8 11 3 6 9 12
+
+getPermutations should return:
+[ "1 2 3 4 5 6 7 8 9 10 11 12", "1 10 8 6 4 2 11 9 7 5 3 12" ]
+*/
+const test12WordGrids = [
+  {
+    testName: "2-column vertical-lr incorrectly recorded as horizontal-tb",
+    words: "1 3 5 7 9 11 2 4 6 8 10 12".split(" "),
+    columns: 2,
+  },
+  {
+    testName: "2-column horizontal-tb incorrectly recorded as vertical-lr",
+    words: "1 7 2 8 3 9 4 10 5 11 6 12".split(" "),
+    columns: 2,
+  },
+  {
+    testName: "3-column vertical-lr incorrectly recorded as horizontal-tb",
+    words: "1 4 7 10 2 5 8 11 3 6 9 12".split(" "),
+    columns: 3,
+  },
+  {
+    testName: "3-column horizontal-tb incorrectly recorded as vertical-lr",
+    words: "1 5 9 2 6 10 3 7 11 4 8 12".split(" "),
+    columns: 3,
+  },
+];
+const test24WordGrids = [
+  {
+    testName: "6-column vertical-lr incorrectly recorded as horizontal-tb",
+    words: "1 7 13 19 2 8 14 20 3 9 15 21 4 10 16 22 5 11 17 23 6 12 18 24"
+      .split(" "),
+    columns: 6,
+  },
+  {
+    testName: "6-column horizontal-tb incorrectly recorded as vertical-lr",
+    words: "1 5 9 13 17 21 2 6 10 14 18 22 3 7 11 15 19 23 4 8 12 16 20 24"
+      .split(" "),
+    columns: 6,
+  },
+  {
+    testName: "8-column vertical-lr incorrectly recorded as horizontal-tb",
+    words: "1 9 17 2 10 18 3 11 19 4 12 20 5 13 21 6 14 22 7 15 23 8 16 24"
+      .split(" "),
+    columns: 8,
+  },
+  {
+    testName: "8-column horizontal-tb incorrectly recorded as vertical-lr",
+    words: "1 4 7 10 13 16 19 22 2 5 8 11 14 17 20 23 3 6 9 12 15 18 21 24"
+      .split(" "),
+    columns: 8,
+  },
+  {
+    testName: "12-column vertical-lr incorrectly recorded as horizontal-tb",
+    words: "1 13 2 14 3 15 4 16 5 17 6 18 7 19 8 20 9 21 10 22 11 23 12 24"
+      .split(" "),
+    columns: 12,
+  },
+  {
+    testName: "12-column horizontal-tb incorrectly recorded as vertical-lr",
+    words: "1 3 5 7 9 11 13 15 17 19 21 23 2 4 6 8 10 12 14 16 18 20 22 24"
+      .split(" "),
+    columns: 12,
+  },
+];
+Deno.test("getPermutations returns correct permutations of 12-word test grids", () => {
+  test12WordGrids.forEach((grid) => {
+    const permutations = getPermutations(grid.words, grid.columns);
+    assertArrayIncludes<string>(permutations, ["1 2 3 4 5 6 7 8 9 10 11 12"]);
+  });
+});
+Deno.test("getPermutations returns correct permutations of 24-word test grids", () => {
+  test24WordGrids.forEach((grid) => {
+    const permutations = getPermutations(grid.words, grid.columns);
+    assertArrayIncludes<string>(permutations, [
+      "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24",
+    ]);
+  });
+});
+
+Deno.test("getLowerFactors returns correct factors of 12", () => {
+  const factors = getLowerFactors(12);
+  assertArrayIncludes<number>(factors, [2, 3]);
+  assertFalse(factors.includes(4));
+  assertFalse(factors.includes(6));
+});
+
+Deno.test("getLowerFactors returns correct factors of 24", () => {
+  const factors = getLowerFactors(24);
+  assertArrayIncludes<number>(factors, [2, 3, 4]);
+  assertFalse(factors.includes(6));
+  assertFalse(factors.includes(8));
+  assertFalse(factors.includes(12));
+});
This page took 0.060816 seconds and 4 git commands to generate.