From 4449bc82c035c9ea4279ec5b07a3e48577a91c05 Mon Sep 17 00:00:00 2001 From: xt0x Date: Sat, 18 Apr 2026 14:41:13 +0900 Subject: [PATCH 1/2] feat(cli): add CLI commands for mnemonic generation, validation, and normalization --- Makefile | 7 +- pom.xml | 13 ++ .../java/com/example/bip39/cli/Bip39Cli.java | 176 ++++++++++++++++++ .../bip39/entropy/SecureEntropyGenerator.java | 2 +- .../example/bip39/util/Bip39Constants.java | 13 ++ .../com/example/bip39/cli/Bip39CliTest.java | 116 ++++++++++++ 6 files changed, 325 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/bip39/cli/Bip39Cli.java create mode 100644 src/test/java/com/example/bip39/cli/Bip39CliTest.java diff --git a/Makefile b/Makefile index 881f83d..0120e46 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ MVNW := ./mvnw -B -ntp -.PHONY: help test verify format lint clean +.PHONY: help test verify format lint clean cli help: @printf "Available targets (requires Java 17):\n" @@ -11,6 +11,7 @@ help: @printf " make format - Apply Spotless formatting\n" @printf " make lint - Run Spotless and SpotBugs checks\n" @printf " make clean - Remove build outputs\n" + @printf " make cli - Build and run the CLI with ARGS=\"...\"\n" test: $(MVNW) test @@ -26,3 +27,7 @@ lint: clean: $(MVNW) clean + +cli: + @$(MVNW) -q -DskipTests package >/dev/null + @java -jar target/bip39-java-0.1.0-SNAPSHOT.jar $(ARGS) diff --git a/pom.xml b/pom.xml index d7cb879..1193bb0 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,7 @@ 2.46.1 4.9.3.0 1.24.0 + 3.4.2 @@ -76,6 +77,18 @@ ${maven.compiler.release} + + org.apache.maven.plugins + maven-jar-plugin + ${maven.jar.plugin.version} + + + + com.example.bip39.cli.Bip39Cli + + + + org.apache.maven.plugins maven-surefire-plugin diff --git a/src/main/java/com/example/bip39/cli/Bip39Cli.java b/src/main/java/com/example/bip39/cli/Bip39Cli.java new file mode 100644 index 0000000..b73365a --- /dev/null +++ b/src/main/java/com/example/bip39/cli/Bip39Cli.java @@ -0,0 +1,176 @@ +package com.example.bip39.cli; + +import com.example.bip39.api.Bip39Service; +import com.example.bip39.api.DefaultBip39Service; +import com.example.bip39.entropy.SecureEntropyGenerator; +import com.example.bip39.error.Bip39Exception; +import com.example.bip39.model.ValidationResult; +import com.example.bip39.normalize.MnemonicInputNormalizer; +import com.example.bip39.util.Bip39Constants; +import java.io.PrintStream; +import java.util.Arrays; +import java.util.HexFormat; + +public final class Bip39Cli { + + private static final String MAIN_CLASS = "com.example.bip39.cli.Bip39Cli"; + + private final Bip39Service bip39Service; + private final SecureEntropyGenerator entropyGenerator; + + public Bip39Cli() { + this(new DefaultBip39Service(), new SecureEntropyGenerator()); + } + + Bip39Cli(Bip39Service bip39Service, SecureEntropyGenerator entropyGenerator) { + this.bip39Service = bip39Service; + this.entropyGenerator = entropyGenerator; + } + + public static void main(String[] args) { + int exitCode = new Bip39Cli().run(args, System.out, System.err); + if (exitCode != 0) { + System.exit(exitCode); + } + } + + int run(String[] args, PrintStream out, PrintStream err) { + if (args.length == 0 || isHelpCommand(args[0])) { + printUsage(out); + return 0; + } + + try { + return switch (args[0]) { + case "generate" -> runGenerate(Arrays.copyOfRange(args, 1, args.length), out); + case "to-entropy" -> runToEntropy(Arrays.copyOfRange(args, 1, args.length), out); + case "to-seed" -> runToSeed(Arrays.copyOfRange(args, 1, args.length), out, err); + case "validate" -> runValidate(Arrays.copyOfRange(args, 1, args.length), out); + case "normalize" -> runNormalize(Arrays.copyOfRange(args, 1, args.length), out); + default -> { + err.println("Unknown command: " + args[0]); + printUsage(err); + yield 2; + } + }; + } catch (IllegalArgumentException exception) { + err.println(exception.getMessage()); + return 2; + } catch (Bip39Exception exception) { + err.println("errorCode=" + exception.getErrorCode()); + err.println("message=" + exception.getMessage()); + return 1; + } + } + + private int runGenerate(String[] args, PrintStream out) { + int mnemonicWordCount = 12; + boolean showEntropy = false; + + for (int index = 0; index < args.length; index++) { + String argument = args[index]; + if ("--words".equals(argument)) { + if (index + 1 >= args.length) { + throw new IllegalArgumentException("Missing value for --words"); + } + mnemonicWordCount = Integer.parseInt(args[++index]); + } else if ("--show-entropy".equals(argument)) { + showEntropy = true; + } else { + throw new IllegalArgumentException("Unknown option for generate: " + argument); + } + } + + int numBytes = Bip39Constants.entropyLengthBytesForMnemonicWordCount(mnemonicWordCount); + byte[] entropy = entropyGenerator.generateEntropy(numBytes); + String mnemonic = bip39Service.entropyToMnemonic(entropy); + if (showEntropy) { + out.println("entropy=" + HexFormat.of().formatHex(entropy)); + } + out.println("mnemonic=" + mnemonic); + return 0; + } + + private int runToEntropy(String[] args, PrintStream out) { + String mnemonic = requireMnemonicArgument(args, "to-entropy"); + out.println(HexFormat.of().formatHex(bip39Service.mnemonicToEntropy(mnemonic))); + return 0; + } + + private int runToSeed(String[] args, PrintStream out, PrintStream err) { + String passphrase = ""; + int mnemonicStart = 0; + + while (mnemonicStart < args.length && args[mnemonicStart].startsWith("--")) { + String option = args[mnemonicStart]; + if (!"--passphrase".equals(option)) { + throw new IllegalArgumentException("Unknown option for to-seed: " + option); + } + if (mnemonicStart + 1 >= args.length) { + throw new IllegalArgumentException("Missing value for --passphrase"); + } + passphrase = args[mnemonicStart + 1]; + mnemonicStart += 2; + } + + if (mnemonicStart >= args.length) { + printUsage(err); + throw new IllegalArgumentException("Missing mnemonic for to-seed"); + } + + String mnemonic = joinArgs(Arrays.copyOfRange(args, mnemonicStart, args.length)); + out.println(HexFormat.of().formatHex(bip39Service.mnemonicToSeed(mnemonic, passphrase))); + return 0; + } + + private int runValidate(String[] args, PrintStream out) { + ValidationResult validationResult = + bip39Service.validateMnemonic(requireMnemonicArgument(args, "validate")); + + out.println("ok=" + validationResult.ok()); + out.println("errorCode=" + nullableValue(validationResult.errorCode())); + out.println("normalizedMnemonic=" + nullableValue(validationResult.normalizedMnemonic())); + out.println("wordCount=" + nullableValue(validationResult.wordCount())); + out.println("invalidWord=" + nullableValue(validationResult.invalidWord())); + return validationResult.ok() ? 0 : 1; + } + + private int runNormalize(String[] args, PrintStream out) { + out.println( + MnemonicInputNormalizer.normalizeMnemonicInput(requireMnemonicArgument(args, "normalize"))); + return 0; + } + + private static String requireMnemonicArgument(String[] args, String command) { + if (args.length == 0) { + throw new IllegalArgumentException("Missing mnemonic for " + command); + } + return joinArgs(args); + } + + private static String joinArgs(String[] args) { + return String.join(" ", args); + } + + private static String nullableValue(Object value) { + return value == null ? "null" : value.toString(); + } + + private static boolean isHelpCommand(String command) { + return "help".equals(command) || "--help".equals(command) || "-h".equals(command); + } + + private static void printUsage(PrintStream out) { + out.println("Usage: java -jar target/bip39-java-0.1.0-SNAPSHOT.jar [options]"); + out.println( + " or: java -cp target/bip39-java-0.1.0-SNAPSHOT.jar " + MAIN_CLASS + " "); + out.println(); + out.println("Commands:"); + out.println(" generate [--words N] [--show-entropy]"); + out.println(" to-entropy "); + out.println(" to-seed [--passphrase TEXT] "); + out.println(" validate "); + out.println(" normalize "); + out.println(" help"); + } +} diff --git a/src/main/java/com/example/bip39/entropy/SecureEntropyGenerator.java b/src/main/java/com/example/bip39/entropy/SecureEntropyGenerator.java index 582b5ee..3729c4f 100644 --- a/src/main/java/com/example/bip39/entropy/SecureEntropyGenerator.java +++ b/src/main/java/com/example/bip39/entropy/SecureEntropyGenerator.java @@ -15,7 +15,7 @@ public SecureEntropyGenerator() { this(new SecureRandom()::nextBytes); } - SecureEntropyGenerator(EntropySource entropySource) { + public SecureEntropyGenerator(EntropySource entropySource) { this.entropySource = Objects.requireNonNull(entropySource, "entropySource must not be null"); } diff --git a/src/main/java/com/example/bip39/util/Bip39Constants.java b/src/main/java/com/example/bip39/util/Bip39Constants.java index 938d5e2..cc4aea4 100644 --- a/src/main/java/com/example/bip39/util/Bip39Constants.java +++ b/src/main/java/com/example/bip39/util/Bip39Constants.java @@ -29,6 +29,19 @@ public static int mnemonicWordCount(int entropyLengthBytes) { return (entropyLengthBits + checksumLengthBits(entropyLengthBytes)) / 11; } + public static int entropyLengthBytesForMnemonicWordCount(int mnemonicWordCount) { + return switch (mnemonicWordCount) { + case 12 -> 16; + case 15 -> 20; + case 18 -> 24; + case 21 -> 28; + case 24 -> 32; + default -> + throw new IllegalArgumentException( + "Unsupported mnemonic word count: " + mnemonicWordCount); + }; + } + public static void validateEntropyLengthBytes(int entropyLengthBytes) { if (!isAllowedEntropyLengthBytes(entropyLengthBytes)) { throw new IllegalArgumentException("Unsupported entropy length bytes: " + entropyLengthBytes); diff --git a/src/test/java/com/example/bip39/cli/Bip39CliTest.java b/src/test/java/com/example/bip39/cli/Bip39CliTest.java new file mode 100644 index 0000000..f2947a2 --- /dev/null +++ b/src/test/java/com/example/bip39/cli/Bip39CliTest.java @@ -0,0 +1,116 @@ +package com.example.bip39.cli; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.example.bip39.api.DefaultBip39Service; +import com.example.bip39.entropy.SecureEntropyGenerator; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class Bip39CliTest { + + private static final String ZERO_ENTROPY_MNEMONIC = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon" + + " about"; + + private final Bip39Cli cli = + new Bip39Cli( + new DefaultBip39Service(), + new SecureEntropyGenerator(bytes -> Arrays.fill(bytes, (byte) 0))); + + @Test + void generatePrintsMnemonicAndOptionalEntropy() { + CommandResult result = run("generate", "--words", "12", "--show-entropy"); + + assertEquals(0, result.exitCode()); + assertTrue(result.stdout().contains("entropy=00000000000000000000000000000000")); + assertTrue(result.stdout().contains("mnemonic=" + ZERO_ENTROPY_MNEMONIC)); + assertEquals("", result.stderr()); + } + + @Test + void toEntropyPrintsHex() { + CommandResult result = run("to-entropy", ZERO_ENTROPY_MNEMONIC); + + assertEquals(0, result.exitCode()); + assertEquals("00000000000000000000000000000000\n", result.stdout()); + } + + @Test + void toSeedPrintsSeedHex() { + CommandResult result = run("to-seed", "--passphrase", "TREZOR", ZERO_ENTROPY_MNEMONIC); + + assertEquals(0, result.exitCode()); + assertTrue( + result + .stdout() + .contains("c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e5349553")); + } + + @Test + void validateReturnsStructuredFailureForInvalidMnemonic() { + CommandResult result = run("validate", "abandon ability able"); + + assertEquals(1, result.exitCode()); + assertTrue(result.stdout().contains("ok=false")); + assertTrue(result.stdout().contains("errorCode=ERR_INVALID_WORD_COUNT")); + assertTrue(result.stdout().contains("wordCount=3")); + } + + @Test + void normalizePrintsCompatibilityNormalizedMnemonic() { + CommandResult result = + run( + "normalize", + " ABANDON\tabandon\nABANDON\rabandon abandon\tabandon\nabandon\rabandon" + + " abandon\tabandon\nABANDON\rABOUT "); + + assertEquals(0, result.exitCode()); + assertEquals(ZERO_ENTROPY_MNEMONIC + "\n", result.stdout()); + } + + @Test + void unknownCommandReturnsUsageError() { + CommandResult result = run("wat"); + + assertEquals(2, result.exitCode()); + assertTrue(result.stderr().contains("Unknown command: wat")); + assertTrue(result.stderr().contains("Commands:")); + } + + @Test + void helpPrintsUsage() { + CommandResult result = run("help"); + + assertEquals(0, result.exitCode()); + assertTrue(result.stdout().contains("generate [--words N] [--show-entropy]")); + assertFalse(result.stdout().isEmpty()); + } + + @Test + void generateRejectsUnsupportedWordCount() { + CommandResult result = run("generate", "--words", "13"); + + assertEquals(2, result.exitCode()); + assertTrue(result.stderr().contains("Unsupported mnemonic word count: 13")); + } + + private CommandResult run(String... args) { + ByteArrayOutputStream stdout = new ByteArrayOutputStream(); + ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + int exitCode = + cli.run( + args, + new PrintStream(stdout, true, StandardCharsets.UTF_8), + new PrintStream(stderr, true, StandardCharsets.UTF_8)); + return new CommandResult( + exitCode, stdout.toString(StandardCharsets.UTF_8), stderr.toString(StandardCharsets.UTF_8)); + } + + private record CommandResult(int exitCode, String stdout, String stderr) {} +} From 2d4fc4d144986b5a11230cd7cfd4ae38959393d9 Mon Sep 17 00:00:00 2001 From: xt0x Date: Sat, 18 Apr 2026 15:09:33 +0900 Subject: [PATCH 2/2] feat(docs): add LICENSE and README files with project overview and CLI usage instructions --- LICENSE | 186 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 402 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a1f6bb5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,186 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the +copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other +entities that control, are controlled by, or are under common control with +that entity. For the purposes of this definition, "control" means (i) the +power, direct or indirect, to cause the direction or management of such +entity, whether by contract or otherwise, or (ii) ownership of fifty percent +(50%) or more of the outstanding shares, or (iii) beneficial ownership of +such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation source, and +configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object +code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, +made available under the License, as indicated by a copyright notice that is +included in or attached to the work (an example is provided in the Appendix +below). + +"Derivative Works" shall mean any work, whether in Source or Object form, +that is based on (or derived from) the Work and for which the editorial +revisions, annotations, elaborations, or other modifications represent, as a +whole, an original work of authorship. For the purposes of this License, +Derivative Works shall not include works that remain separable from, or merely +link (or bind by name) to the interfaces of, the Work and Derivative Works +thereof. + +"Contribution" shall mean any work of authorship, including the original +version of the Work and any modifications or additions to that Work or +Derivative Works thereof, that is intentionally submitted to Licensor for +inclusion in the Work by the copyright owner or by an individual or Legal +Entity authorized to submit on behalf of the copyright owner. For the purposes +of this definition, "submitted" means any form of electronic, verbal, or +written communication sent to the Licensor or its representatives, including +but not limited to communication on electronic mailing lists, source code +control systems, and issue tracking systems that are managed by, or on behalf +of, the Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise designated in +writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this +License, each Contributor hereby grants to You a perpetual, worldwide, +non-exclusive, no-charge, royalty-free, irrevocable copyright license to +reproduce, prepare Derivative Works of, publicly display, publicly perform, +sublicense, and distribute the Work and such Derivative Works in Source or +Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this +License, each Contributor hereby grants to You a perpetual, worldwide, +non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this +section) patent license to make, have made, use, offer to sell, sell, import, +and otherwise transfer the Work, where such license applies only to those +patent claims licensable by such Contributor that are necessarily infringed by +their Contribution(s) alone or by combination of their Contribution(s) with the +Work to which such Contribution(s) was submitted. If You institute patent +litigation against any entity (including a cross-claim or counterclaim in a +lawsuit) alleging that the Work or a Contribution incorporated within the Work +constitutes direct or contributory patent infringement, then any patent +licenses granted to You under this License for that Work shall terminate as of +the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or +Derivative Works thereof in any medium, with or without modifications, and in +Source or Object form, provided that You meet the following conditions: + +(a) You must give any other recipients of the Work or Derivative Works a copy +of this License; and + +(b) You must cause any modified files to carry prominent notices stating that +You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works that You +distribute, all copyright, patent, trademark, and attribution notices from the +Source form of the Work, excluding those notices that do not pertain to any +part of the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its distribution, then +any Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of +the following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a +whole, provided Your use, reproduction, and distribution of the Work otherwise +complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any +Contribution intentionally submitted for inclusion in the Work by You to the +Licensor shall be under the terms and conditions of this License, without any +additional terms or conditions. Notwithstanding the above, nothing herein shall +supersede or modify the terms of any separate license agreement you may have +executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, +trademarks, service marks, or product names of the Licensor, except as +required for reasonable and customary use in describing the origin of the Work +and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in +writing, Licensor provides the Work (and each Contributor provides its +Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied, including, without limitation, any warranties +or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any risks +associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in +_tort_ (including negligence), contract, or otherwise, unless required by +applicable law (such as deliberate and grossly negligent acts) or agreed to in +writing, shall any Contributor be liable to You for damages, including any +direct, indirect, special, incidental, or consequential damages of any +character arising as a result of this License or out of the use or inability to +use the Work (including but not limited to damages for loss of goodwill, work +stoppage, computer failure or malfunction, or any and all other commercial +damages or losses), even if such Contributor has been advised of the +possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or +Derivative Works thereof, You may choose to offer, and charge a fee for, +acceptance of support, warranty, indemnity, or other liability obligations +and/or rights consistent with this License. However, in accepting such +obligations, You may act only on Your own behalf and on Your sole +responsibility, not on behalf of any other Contributor, and only if You agree +to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification +within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..40511f6 --- /dev/null +++ b/README.md @@ -0,0 +1,216 @@ +
+

BIP39 Java

+

Java 17 library and CLI for generating, validating, and converting BIP39 English mnemonics

+ License + Java + Maven + CLI + +
+ +--- + +# Project Overview + +This repository provides the following capabilities for English BIP39 mnemonics: + +- Convert entropy to a mnemonic sentence +- Convert a mnemonic sentence back to entropy +- Derive a 64-byte seed from a mnemonic and passphrase +- Validate mnemonic structure, word membership, and checksum +- Normalize compatibility input explicitly +- Generate entropy with a secure random source +- Use the implementation from a Java CLI + +## Scope + +This repository targets **BIP39** only. + +- The supported wordlist is English only +- BIP32, derivation paths, address generation, and full wallet UX are out of scope +- `mnemonicToSeed` intentionally does not validate mnemonic semantic correctness by itself + +## Requirements + +- Java 17 +- Maven 3.9.6 or later + +The repository includes the Maven Wrapper, so `./mvnw` is the preferred way to run builds. + +## Setup + +```bash +./mvnw -B -ntp test +``` + +You can also use the provided `Makefile`. + +```bash +make test +make verify +``` + +Main targets: + +- `make test` - Run unit tests +- `make verify` - Run Spotless, SpotBugs, and tests +- `make format` - Apply Spotless formatting +- `make lint` - Run Spotless and SpotBugs checks +- `make clean` - Remove build outputs +- `make cli ARGS="..."` - Run the CLI + +## CLI + +The CLI entry point is `src/main/java/com/example/bip39/cli/Bip39Cli.java`. + +### Show help + +```bash +make cli ARGS="help" +``` + +### Generate a 12-word mnemonic + +```bash +make cli ARGS="generate --words 12" +``` + +### Generate a 24-word mnemonic and print entropy too + +```bash +make cli ARGS="generate --words 24 --show-entropy" +``` + +### Convert a mnemonic back to entropy + +```bash +make cli ARGS='to-entropy "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"' +``` + +### Derive a seed from a mnemonic and passphrase + +```bash +make cli ARGS='to-seed --passphrase TREZOR "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"' +``` + +### Validate a mnemonic + +```bash +make cli ARGS='validate "abandon ability able"' +``` + +### Normalize compatibility input + +```bash +make cli ARGS=$'normalize " ABANDON\tabandon\nABANDON\rABOUT "' +``` + +### Run the JAR directly + +```bash +./mvnw -B -ntp -DskipTests package +java -jar target/bip39-java-0.1.0-SNAPSHOT.jar help +``` + +## Java API + +The public API is defined in `src/main/java/com/example/bip39/api/Bip39Service.java`. + +```java +import com.example.bip39.api.Bip39Service; +import com.example.bip39.api.DefaultBip39Service; +import java.util.HexFormat; + +Bip39Service service = new DefaultBip39Service(); + +byte[] entropy = new byte[16]; +String mnemonic = service.entropyToMnemonic(entropy); +byte[] roundTrip = service.mnemonicToEntropy(mnemonic); +byte[] seed = service.mnemonicToSeed(mnemonic, "TREZOR"); + +System.out.println(mnemonic); +System.out.println(HexFormat.of().formatHex(roundTrip)); +System.out.println(seed.length); +System.out.println(service.validateMnemonic(mnemonic)); +``` + +## Implementation Notes + +### Strict parsing APIs + +`mnemonicToEntropy` and `validateMnemonic` are strict. + +- They only accept normalized lowercase English mnemonics +- Tabs, newlines, repeated spaces, and leading or trailing spaces fail as-is +- If needed, run the input through the normalization layer first + +### Compatibility normalization + +The compatibility input adapter lives in `src/main/java/com/example/bip39/normalize/MnemonicInputNormalizer.java`. + +It performs the following steps: + +- Trim leading and trailing whitespace +- Replace tabs, newlines, and carriage returns with `SPACE` +- Collapse repeated spaces to a single `SPACE` +- Apply Unicode NFKD +- Lowercase for the English profile + +### Seed derivation + +`mnemonicToSeed` follows the BIP39 contract. + +- It returns 64 bytes +- It uses NFKD + UTF-8 + PBKDF2-HMAC-SHA512 +- It does not automatically validate mnemonic semantic correctness + +Call `validateMnemonic` first if your application requires validation before seed derivation. + +## Testing + +This repository includes tests for: + +- English wordlist and pinned asset integrity +- Bit-level conversion helpers +- Official `entropyToMnemonic` vectors +- Official `mnemonicToEntropy` vectors +- Official `mnemonicToSeed` vectors +- Fixed failure cases and error priority +- Compatibility normalization +- Entropy generation boundaries +- CLI behavior + +## Structure + +```text +. +├── docs/ # Specifications and implementation plans +├── src/main/java/com/example/bip39/ +│ ├── api/ # Public API +│ ├── bit/ # Bit-level helpers +│ ├── cli/ # CLI +│ ├── crypto/ # SHA-256 / PBKDF2-HMAC-SHA512 +│ ├── entropy/ # SecureRandom-based entropy generation +│ ├── error/ # Error codes and exceptions +│ ├── integration/ # Thin UI / external integration helpers +│ ├── model/ # ValidationResult and related types +│ ├── normalize/ # Compatibility normalization +│ ├── parser/ # Strict mnemonic parsing +│ ├── util/ # Constants +│ └── wordlist/ # English wordlist handling +├── src/main/resources/bip39/ # Pinned normative assets +├── src/test/java/com/example/bip39/ # Tests +├── Makefile +├── pom.xml +└── README.md +``` + +## Security Notes + +- Generated mnemonics and seeds are secrets +- Do not log them, screenshot them, or commit them to source control +- All command examples in this README assume Java 17 + +## License + +Apache License 2.0 - see [LICENSE](LICENSE).