From: Ardis Lu Date: Sun, 3 Sep 2023 07:32:29 +0000 (-0700) Subject: Initial commit X-Git-Url: https://git.ardis.lu/?a=commitdiff_plain;h=eb65b089c5d3bc1cf862fd8cf5486b3311f9ce30;p=seed-phrase-transposer Initial commit --- eb65b089c5d3bc1cf862fd8cf5486b3311f9ce30 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..722d5e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode diff --git a/README.md b/README.md new file mode 100644 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 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, 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 index 0000000..e4267be --- /dev/null +++ b/main_test.ts @@ -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(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(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(factors, [2, 3]); + assertFalse(factors.includes(4)); + assertFalse(factors.includes(6)); +}); + +Deno.test("getLowerFactors returns correct factors of 24", () => { + const factors = getLowerFactors(24); + assertArrayIncludes(factors, [2, 3, 4]); + assertFalse(factors.includes(6)); + assertFalse(factors.includes(8)); + assertFalse(factors.includes(12)); +});