From ce7bd27de0da33eea68896d1e5f7f27a0e8a197e Mon Sep 17 00:00:00 2001 From: Benjamin Dweck Date: Fri, 12 Mar 2021 12:36:02 +0200 Subject: [PATCH] Implemented support for custom binary entropy source --- .../rudefox/burrow/Dice8EntropyGenerator.java | 148 ------------- ...{DiceEventBuffer.java => EventBuffer.java} | 12 +- .../burrow/InteractiveEntropyGenerator.java | 200 ++++++++++++++++++ .../io/rudefox/burrow/MnemonicCommand.java | 54 +++-- .../io/rudefox/burrow/coin_entropy_tests.java | 62 ++++++ 5 files changed, 302 insertions(+), 174 deletions(-) delete mode 100644 src/main/java/io/rudefox/burrow/Dice8EntropyGenerator.java rename src/main/java/io/rudefox/burrow/{DiceEventBuffer.java => EventBuffer.java} (80%) create mode 100644 src/main/java/io/rudefox/burrow/InteractiveEntropyGenerator.java create mode 100644 src/test/java/io/rudefox/burrow/coin_entropy_tests.java diff --git a/src/main/java/io/rudefox/burrow/Dice8EntropyGenerator.java b/src/main/java/io/rudefox/burrow/Dice8EntropyGenerator.java deleted file mode 100644 index 5b6cbc0..0000000 --- a/src/main/java/io/rudefox/burrow/Dice8EntropyGenerator.java +++ /dev/null @@ -1,148 +0,0 @@ -package io.rudefox.burrow; - -import com.bjdweck.bitcoin.mnemonic.Entropy; -import com.diogonunes.jcolor.AnsiFormat; - -import java.util.Scanner; -import java.util.regex.Pattern; - -import static com.diogonunes.jcolor.Attribute.*; - -public class Dice8EntropyGenerator { - - public static final AnsiFormat RED_STYLE = new AnsiFormat(WHITE_TEXT(), RED_BACK(), BOLD()); - public static final AnsiFormat GREEN_STYLE = new AnsiFormat(BLACK_TEXT(), GREEN_BACK(), BOLD()); - public static final AnsiFormat BLUE_STYLE = new AnsiFormat(WHITE_TEXT(), BLUE_BACK(), BOLD()); - - public static final int DICE_PER_ROLL = 11; - - private final int targetBitsOfEntropy; - private final Scanner inputScanner; - private final boolean ansiColorOutput; - - private Entropy entropy = new Entropy(); - - public Dice8EntropyGenerator(int targetBitsOfEntropy, Scanner inputScanner, boolean ansiColorOutput) { - - this.targetBitsOfEntropy = targetBitsOfEntropy; - this.inputScanner = inputScanner; - this.ansiColorOutput = ansiColorOutput; - } - - Entropy generate() { - - entropy = new Entropy(); - - for (int rollSetCount = 0; entropy.getBitLength() < targetBitsOfEntropy; rollSetCount++) - doDiceRoll(rollSetCount); - - return entropy.truncate(targetBitsOfEntropy).appendChecksum(); - } - - private void doDiceRoll(int currentRollSet) { - - String eventString = scanNextRollSetString(); - - StringBuilder rollValuesLine = new StringBuilder("|"); - - for (int rollNumber = 0; rollNumber < DICE_PER_ROLL; rollNumber++) { - - int rollValue = Integer.parseInt(eventString.charAt(rollNumber) + ""); - - String formatString = - rollNumber == 3 || rollNumber == 7 ? " %d |" : " %d |"; - - rollValuesLine.append(String.format(formatString, rollValue)); - - entropy = entropy.appendBits(rollValue - 1, 3); - } - - System.out.printf("%n%s%n%s%n%s%n%n", - rollValuesLine, - getBitsLine(), - getSeedWordsLine(currentRollSet) - ); - } - - private String scanNextRollSetString() { - - String rollSetString = ""; - Pattern rollSetPattern = Pattern.compile(String.format("[1-8]{%d}", DICE_PER_ROLL)); - - while (!rollSetPattern.matcher(rollSetString).matches()) { - System.out.print("Input 11 x 8-sided dice rolls [1-8]: "); - rollSetString = inputScanner.next(); - } - - return rollSetString; - } - - private StringBuilder getBitsLine() { - - String entropyBitString = entropy.toString(); - - String currentRollSetBitString = - entropyBitString.substring(entropyBitString.length() - (DICE_PER_ROLL * 3)); - - StringBuilder rollSetBitsLine = new StringBuilder("|"); - for (int rollSetBitIndex = 0; rollSetBitIndex < currentRollSetBitString.length(); rollSetBitIndex++) { - - int entropyBitIndex = entropyBitString.length() - currentRollSetBitString.length() + rollSetBitIndex; - - if (entropyBitIndex < targetBitsOfEntropy) - rollSetBitsLine.append(styleBit("" + currentRollSetBitString.charAt(rollSetBitIndex), entropyBitIndex, rollSetBitIndex)); - else - rollSetBitsLine.append("-"); - - if (rollSetBitIndex == 32) - rollSetBitsLine.append("|"); - else if (rollSetBitIndex % 3 == 2) - rollSetBitsLine.append(styleBit(" ", entropyBitIndex, rollSetBitIndex)); - else if (rollSetBitIndex % 11 == 10) - rollSetBitsLine.append(" | "); - } - - return rollSetBitsLine; - } - - private String styleBit(String bit, int globalEntropyBitIndex, int currentRollSetBitIndex) { - - if (!ansiColorOutput || globalEntropyBitIndex >= targetBitsOfEntropy) - return bit; - - return getBitFormat(currentRollSetBitIndex).format(bit); - } - - private AnsiFormat getBitFormat(int rollSetBitIndex) { - - int wordBitIndex = rollSetBitIndex % 11; - - if (wordBitIndex == 0) - return RED_STYLE; - - if (wordBitIndex <= 6) - return GREEN_STYLE; - - return BLUE_STYLE; - } - - private String getSeedWordsLine(int rollSet) { - - int baseIndex = rollSet * 3; - - return String.format( - "|%-15s|%-17s|%-15s|", - getFormattedSeedWord(baseIndex), - getFormattedSeedWord(baseIndex + 1), - getFormattedSeedWord(baseIndex + 2)); - } - - private String getFormattedSeedWord(int index) { - - int lastWordIndex = (3 * targetBitsOfEntropy / 32) - 1; - - String seedWord = index != lastWordIndex ? entropy.getWord(index) : "CHECKWORD"; - - return String.format(" %2d. %s", index + 1, seedWord); - } -} \ No newline at end of file diff --git a/src/main/java/io/rudefox/burrow/DiceEventBuffer.java b/src/main/java/io/rudefox/burrow/EventBuffer.java similarity index 80% rename from src/main/java/io/rudefox/burrow/DiceEventBuffer.java rename to src/main/java/io/rudefox/burrow/EventBuffer.java index 69cc0fd..13512f7 100644 --- a/src/main/java/io/rudefox/burrow/DiceEventBuffer.java +++ b/src/main/java/io/rudefox/burrow/EventBuffer.java @@ -3,21 +3,21 @@ package io.rudefox.burrow; import com.bjdweck.bitcoin.mnemonic.Entropy; import com.bjdweck.math.UnsignedInt; -class DiceEventBuffer { +class EventBuffer { - private final int diceBase; + private final int eventBase; private final StringBuilder buffer; private final int targetBitsOfEntropy; - DiceEventBuffer(int targetBitsOfEntropy, int diceBase) { + EventBuffer(int targetBitsOfEntropy, int eventBase) { - this.diceBase = diceBase; + this.eventBase = eventBase; this.targetBitsOfEntropy = targetBitsOfEntropy; this.buffer = new StringBuilder(getRequiredEvents()); } int getRequiredEvents() { - return (int) Math.ceil(this.targetBitsOfEntropy * Math.log(2) / Math.log(diceBase)); + return (int) Math.ceil(this.targetBitsOfEntropy * Math.log(2) / Math.log(eventBase)); } void appendEvents(String eventString) { @@ -27,7 +27,7 @@ class DiceEventBuffer { } Entropy toEntropy() { - UnsignedInt entropy = new UnsignedInt(toString(), diceBase); + UnsignedInt entropy = new UnsignedInt(toString(), eventBase); byte[] entropyBytes = entropy.getLowestOrderBits(this.targetBitsOfEntropy).toBigEndianByteArray(); return Entropy.fromRawEntropy(entropyBytes); } diff --git a/src/main/java/io/rudefox/burrow/InteractiveEntropyGenerator.java b/src/main/java/io/rudefox/burrow/InteractiveEntropyGenerator.java new file mode 100644 index 0000000..0358400 --- /dev/null +++ b/src/main/java/io/rudefox/burrow/InteractiveEntropyGenerator.java @@ -0,0 +1,200 @@ +package io.rudefox.burrow; + +import com.bjdweck.bitcoin.mnemonic.Entropy; +import com.diogonunes.jcolor.AnsiFormat; + +import java.util.Scanner; +import java.util.regex.Pattern; + +import static com.diogonunes.jcolor.Attribute.*; + +public class InteractiveEntropyGenerator { + + public static final int BITS_PER_WORD = 11; + public static final int WORDS_PER_EVENT_SET = 3; + + enum EntropyBase { TWO, EIGHT } + + public static final AnsiFormat RED_STYLE = new AnsiFormat(WHITE_TEXT(), RED_BACK(), BOLD()); + public static final AnsiFormat GREEN_STYLE = new AnsiFormat(BLACK_TEXT(), GREEN_BACK(), BOLD()); + public static final AnsiFormat BLUE_STYLE = new AnsiFormat(WHITE_TEXT(), BLUE_BACK(), BOLD()); + + private final EntropyBase entropyBase; + private final int eventsPerSet; + private final int bitsPerEvent; + private final int targetBitsOfEntropy; + private final Scanner inputScanner; + private final boolean ansiColorOutput; + + private Entropy entropy = new Entropy(); + + public InteractiveEntropyGenerator(int targetBitsOfEntropy, Scanner inputScanner, boolean ansiColorOutput, EntropyBase entropyBase) { + + this.targetBitsOfEntropy = targetBitsOfEntropy; + this.inputScanner = inputScanner; + this.ansiColorOutput = ansiColorOutput; + this.entropyBase = entropyBase; + this.bitsPerEvent = entropyBase == EntropyBase.EIGHT ? 3 : 1; + this.eventsPerSet = BITS_PER_WORD * WORDS_PER_EVENT_SET / bitsPerEvent; + } + + Entropy generate() { + + entropy = new Entropy(); + + for (int eventSetCount = 0; entropy.getBitLength() < targetBitsOfEntropy; eventSetCount++) + doEventSet(eventSetCount); + + return entropy.truncate(targetBitsOfEntropy).appendChecksum(); + } + + private void doEventSet(int currentEventSet) { + + String eventString = + this.entropyBase == EntropyBase.EIGHT ? + readNextDice8EventSetString() : + readNextBinaryEventSetString(); + + StringBuilder eventValuesLine = new StringBuilder("|"); + + for (int eventIndex = 0; eventIndex < eventsPerSet; eventIndex++) { + + int eventValue = Integer.parseInt(eventString.charAt(eventIndex) + ""); + + String formatString = eventIndex == bitsPerEvent || eventIndex == 7 ? " %d |" : " %d |"; + + eventValuesLine.append(String.format(formatString, eventValue)); + + if (entropyBase == EntropyBase.EIGHT) + eventValue--; + + entropy = entropy.appendBits(eventValue, bitsPerEvent); + } + + if (entropyBase == EntropyBase.EIGHT) { + System.out.printf("%n%s%n%s%n%s%n%n", + eventValuesLine, + getBitsLine(), + getSeedWordsLine(currentEventSet) + ); + } else { + System.out.printf("%n%s%n%s%n%n", + getBitsLine(), + getSeedWordsLine(currentEventSet) + ); + } + } + + private String readNextDice8EventSetString() { + + String eventSetString = ""; + Pattern eventSetPattern = Pattern.compile(String.format("[1-8]{%d}", eventsPerSet)); + + while (!eventSetPattern.matcher(eventSetString).matches()) { + System.out.print("Input 11 x 8-sided dice rolls [1-8]: "); + eventSetString = inputScanner.next(); + } + + return eventSetString; + } + + private String readNextBinaryEventSetString() { + + String eventSetString = ""; + Pattern eventSetPattern = Pattern.compile(String.format("[0-1]{%d}", eventsPerSet)); + + while (!eventSetPattern.matcher(eventSetString).matches()) { + System.out.print("Input 33 coin tosses [0-1]: "); + eventSetString = inputScanner.next(); + } + + return eventSetString; + } + + private StringBuilder getBitsLine() { + + String entropyBitString = entropy.toString(); + + String currentEventSetBitString = + entropyBitString.substring(entropyBitString.length() - (BITS_PER_WORD * WORDS_PER_EVENT_SET)); + + StringBuilder eventSetBitsLine = new StringBuilder("|"); + + if (entropyBase == EntropyBase.TWO) + eventSetBitsLine.append(" "); + + for (int eventSetBitIndex = 0; eventSetBitIndex < currentEventSetBitString.length(); eventSetBitIndex++) { + + int entropyBitIndex = entropyBitString.length() - currentEventSetBitString.length() + eventSetBitIndex; + + if (entropyBitIndex < targetBitsOfEntropy) + eventSetBitsLine.append(styleBit("" + currentEventSetBitString.charAt(eventSetBitIndex), entropyBitIndex, eventSetBitIndex)); + else + eventSetBitsLine.append("-"); + + if (entropyBase == EntropyBase.EIGHT) { + if (eventSetBitIndex == 32) + eventSetBitsLine.append("|"); + else if (eventSetBitIndex % bitsPerEvent == bitsPerEvent - 1) + eventSetBitsLine.append(styleBit(" ", entropyBitIndex, eventSetBitIndex)); + else if (eventSetBitIndex % BITS_PER_WORD == BITS_PER_WORD - 1) + eventSetBitsLine.append(" | "); + } + + if (entropyBase == EntropyBase.TWO) { + if (eventSetBitIndex == 32) + eventSetBitsLine.append(" |"); + else if (eventSetBitIndex % BITS_PER_WORD == 0 || eventSetBitIndex % BITS_PER_WORD == 6) + eventSetBitsLine.append(styleBit(" ", entropyBitIndex, eventSetBitIndex)); + else if (eventSetBitIndex % BITS_PER_WORD == BITS_PER_WORD - 1) + eventSetBitsLine.append(" | "); + } + } + + return eventSetBitsLine; + } + + private String styleBit(String bit, int globalEntropyBitIndex, int currentRollSetBitIndex) { + + if (!ansiColorOutput || globalEntropyBitIndex >= targetBitsOfEntropy) + return bit; + + return getBitFormat(currentRollSetBitIndex).format(bit); + } + + private AnsiFormat getBitFormat(int eventSetBitIndex) { + + int wordBitIndex = eventSetBitIndex % BITS_PER_WORD; + + if (wordBitIndex == 0) + return RED_STYLE; + + if (wordBitIndex <= 6) + return GREEN_STYLE; + + return BLUE_STYLE; + } + + private String getSeedWordsLine(int rollSet) { + + int baseIndex = rollSet * WORDS_PER_EVENT_SET; + + String format = entropyBase == EntropyBase.EIGHT ? + "|%-15s|%-17s|%-15s|" : "|%-15s|%-15s|%-15s|"; + + return String.format( + format, + getFormattedSeedWord(baseIndex), + getFormattedSeedWord(baseIndex + 1), + getFormattedSeedWord(baseIndex + 2)); + } + + private String getFormattedSeedWord(int index) { + + int lastWordIndex = (WORDS_PER_EVENT_SET * targetBitsOfEntropy / 32) - 1; + + String seedWord = index != lastWordIndex ? entropy.getWord(index) : "CHECKWORD"; + + return String.format(" %2d. %s", index + 1, seedWord); + } +} \ No newline at end of file diff --git a/src/main/java/io/rudefox/burrow/MnemonicCommand.java b/src/main/java/io/rudefox/burrow/MnemonicCommand.java index 94477cc..8053932 100644 --- a/src/main/java/io/rudefox/burrow/MnemonicCommand.java +++ b/src/main/java/io/rudefox/burrow/MnemonicCommand.java @@ -11,14 +11,19 @@ public class MnemonicCommand implements Runnable { private static final int DEFAULT_BITS_OF_ENTROPY = 256; - private boolean isDiceEntropy() { - return entropyOptions != null && (entropyOptions.isDice6Entropy || entropyOptions.isDice8Entropy); + private boolean isCustomEntropy() { + return entropyOptions != null && + (entropyOptions.isDice6Entropy || entropyOptions.isDice8Entropy || entropyOptions.isBinaryEntropy); } private boolean isInteractiveMode() { return entropyOptions != null && entropyOptions.eventMethod.isInteractiveMode; } + private boolean isBinaryInteractiveMode() { + return isInteractiveMode() && entropyOptions.isBinaryEntropy; + } + private boolean isDice6InteractiveMode() { return isInteractiveMode() && entropyOptions.isDice6Entropy; } @@ -32,6 +37,10 @@ public class MnemonicCommand implements Runnable { static class EntropyOptions { + @CommandLine.Option(names = {"-2", "--binary-entropy"}, + description = "use a coin or other binary entropy source") + boolean isBinaryEntropy; + @CommandLine.Option(names = {"-6", "--dice6-entropy"}, description = "use 6-sided dice entropy source") boolean isDice6Entropy; @@ -71,50 +80,55 @@ public class MnemonicCommand implements Runnable { Entropy getEntropy() { - if (!isDiceEntropy()) - return getGeneratedEntropy(); + if (!isCustomEntropy()) + return generateEntropy(); - DiceEventBuffer diceEventBuffer; + EventBuffer eventBuffer; - if (entropyOptions.isDice6Entropy) - diceEventBuffer = new DiceEventBuffer(this.targetBitsOfEntropy, 6); + if (entropyOptions.isBinaryEntropy) + eventBuffer = new EventBuffer(this.targetBitsOfEntropy, 2); + else if (entropyOptions.isDice6Entropy) + eventBuffer = new EventBuffer(this.targetBitsOfEntropy, 6); else - diceEventBuffer = new DiceEventBuffer(this.targetBitsOfEntropy, 8); + eventBuffer = new EventBuffer(this.targetBitsOfEntropy, 8); if (isDice6InteractiveMode()) - return getDice6EntropyInteractive(diceEventBuffer); + return getDice6EntropyInteractive(eventBuffer); if (isDice8InteractiveMode()) - return new Dice8EntropyGenerator(targetBitsOfEntropy, new Scanner(System.in), CommandLine.Help.Ansi.AUTO.enabled()).generate(); + return new InteractiveEntropyGenerator(targetBitsOfEntropy, new Scanner(System.in), CommandLine.Help.Ansi.AUTO.enabled(), InteractiveEntropyGenerator.EntropyBase.EIGHT).generate(); - diceEventBuffer.appendEvents(entropyOptions.eventMethod.getEventString); - return diceEventBuffer.toEntropy(); + if (isBinaryInteractiveMode()) + return new InteractiveEntropyGenerator(targetBitsOfEntropy, new Scanner(System.in), CommandLine.Help.Ansi.AUTO.enabled(), InteractiveEntropyGenerator.EntropyBase.TWO).generate(); + + eventBuffer.appendEvents(entropyOptions.eventMethod.getEventString); + return eventBuffer.toEntropy(); } - private Entropy getDice6EntropyInteractive(DiceEventBuffer diceEventBuffer) { + private Entropy getDice6EntropyInteractive(EventBuffer eventBuffer) { - int requiredEvents = diceEventBuffer.getRequiredEvents(); + int requiredEvents = eventBuffer.getRequiredEvents(); Scanner scanner = new Scanner(System.in); boolean firstIteration = true; - while (diceEventBuffer.events() < requiredEvents) { + while (eventBuffer.events() < requiredEvents) { - int remainingEvents = requiredEvents - diceEventBuffer.events(); - System.out.print(String.format("Input %d %sdice rolls [1-6]: ", remainingEvents, (firstIteration ? "" : "more "))); + int remainingEvents = requiredEvents - eventBuffer.events(); + System.out.printf("Input %d %sdice rolls [1-6]: ", remainingEvents, (firstIteration ? "" : "more ")); String inputString = scanner.next(); - diceEventBuffer.appendEvents(inputString); + eventBuffer.appendEvents(inputString); firstIteration = false; } - return diceEventBuffer.toEntropy(); + return eventBuffer.toEntropy(); } - private Entropy getGeneratedEntropy() { + private Entropy generateEntropy() { SecureRandom random = new SecureRandom(); int byteLength = targetBitsOfEntropy / 8; diff --git a/src/test/java/io/rudefox/burrow/coin_entropy_tests.java b/src/test/java/io/rudefox/burrow/coin_entropy_tests.java new file mode 100644 index 0000000..4e53700 --- /dev/null +++ b/src/test/java/io/rudefox/burrow/coin_entropy_tests.java @@ -0,0 +1,62 @@ +package io.rudefox.burrow; + +import com.bjdweck.test.CliTestFixture; +import org.junit.jupiter.api.Test; + +import java.io.UnsupportedEncodingException; + +class coin_entropy_tests extends CliTestFixture { + + @Test + void with_arguments_interactive_coin_should_generate_mnemonic_sentence() throws UnsupportedEncodingException { + + System.setProperty("picocli.ansi", "false"); + + withArgs("mnemonic -i2 --bits 128"); + + expectedOutput("Input 33 coin tosses [0-1]: "); + provideInput("000001010011100101110111000001010" + EOL); + + expectedOutput(EOL); + expectedOutput("| 0 000010 1001 | 1 100101 1101 | 1 100000 1010 |" + EOL); + expectedOutput("| 1. ahead | 2. slight | 3. scout |" + EOL); + expectedOutput(EOL); + + expectedOutput("Input 33 coin tosses [0-1]: "); + provideInput("000001010011100101110111000001010" + EOL); + + expectedOutput(EOL); + expectedOutput("| 0 000010 1001 | 1 100101 1101 | 1 100000 1010 |" + EOL); + expectedOutput("| 4. ahead | 5. slight | 6. scout |" + EOL); + expectedOutput(EOL); + + expectedOutput("Input 33 coin tosses [0-1]: "); + provideInput("000001010011100101110111000001010" + EOL); + + expectedOutput(EOL); + expectedOutput("| 0 000010 1001 | 1 100101 1101 | 1 100000 1010 |" + EOL); + expectedOutput("| 7. ahead | 8. slight | 9. scout |" + EOL); + expectedOutput(EOL); + + expectedOutput("Input 33 coin tosses [0-1]: "); + provideInput("000001010011100101110111000001010" + EOL); + + expectedOutput(EOL); + expectedOutput("| 0 000010 1001 | 1 100101 1101 | 1 100000 ---- |" + EOL); + expectedOutput("| 10. ahead | 11. slight | 12. CHECKWORD |" + EOL); + expectedOutput(EOL); + + expectedOutput("ahead slight scout ahead slight scout ahead slight scout ahead slight scan" + EOL); + + doMain(); + + verifyOutput(); + } + + private void doMain() { + + setInput(); + + RudefoxBurrow.main(getArgs()); + } +} \ No newline at end of file