Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CI

on:
push:
branches:
- main
pull_request:

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
verify:
name: Lint, build, and test
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Temurin JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: maven

- name: Run lint, build, and tests
run: make verify
28 changes: 28 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.DEFAULT_GOAL := help

MVNW := ./mvnw -B -ntp

.PHONY: help test verify format lint clean

help:
@printf "Available targets (requires Java 17):\n"
@printf " make test - Run unit tests\n"
@printf " make verify - Run lint, build, and tests\n"
@printf " make format - Apply Spotless formatting\n"
@printf " make lint - Run Spotless and SpotBugs checks\n"
@printf " make clean - Remove build outputs\n"

test:
$(MVNW) test

verify:
$(MVNW) verify

format:
$(MVNW) spotless:apply

lint:
$(MVNW) spotless:check spotbugs:check

clean:
$(MVNW) clean
50 changes: 49 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
<maven.compiler.plugin.version>3.13.0</maven.compiler.plugin.version>
<maven.surefire.plugin.version>3.2.5</maven.surefire.plugin.version>
<maven.enforcer.plugin.version>3.5.0</maven.enforcer.plugin.version>
<spotless.maven.plugin.version>2.46.1</spotless.maven.plugin.version>
<spotbugs.maven.plugin.version>4.9.3.0</spotbugs.maven.plugin.version>
<google.java.format.version>1.24.0</google.java.format.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -52,7 +55,10 @@
<configuration>
<rules>
<requireJavaVersion>
<version>[17,)</version>
<version>[17,18)</version>
<message>
Build and verification must run on Java 17. Newer JDKs can break SpotBugs analysis.
</message>
</requireJavaVersion>
<requireMavenVersion>
<version>[3.9.6,)</version>
Expand All @@ -78,6 +84,48 @@
<useModulePath>false</useModulePath>
</configuration>
</plugin>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>${spotless.maven.plugin.version}</version>
<configuration>
<java>
<googleJavaFormat>
<version>${google.java.format.version}</version>
</googleJavaFormat>
<removeUnusedImports />
<trimTrailingWhitespace />
<endWithNewline />
</java>
</configuration>
<executions>
<execution>
<id>spotless-check</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>${spotbugs.maven.plugin.version}</version>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
</configuration>
<executions>
<execution>
<id>spotbugs-check</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
21 changes: 21 additions & 0 deletions src/main/java/com/example/bip39/api/Bip39Service.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.bip39.api;

import com.example.bip39.model.ValidationResult;
import java.util.List;

public interface Bip39Service {

String entropyToMnemonic(byte[] entropy);

byte[] mnemonicToEntropy(String mnemonic);

byte[] mnemonicToEntropy(List<String> words);

ValidationResult validateMnemonic(String mnemonic);

ValidationResult validateMnemonic(List<String> words);

byte[] mnemonicToSeed(String mnemonic, String passphrase);

byte[] mnemonicToSeed(List<String> words, String passphrase);
}
59 changes: 59 additions & 0 deletions src/main/java/com/example/bip39/api/DefaultBip39Service.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.example.bip39.api;

import com.example.bip39.model.ValidationResult;
import com.example.bip39.wordlist.Bip39WordList;
import com.example.bip39.wordlist.EnglishWordList;
import java.util.List;
import java.util.Objects;

public final class DefaultBip39Service implements Bip39Service {

private final Bip39WordList wordList;

public DefaultBip39Service() {
this(EnglishWordList.getInstance());
}

DefaultBip39Service(Bip39WordList wordList) {
this.wordList = Objects.requireNonNull(wordList, "wordList must not be null");
}

Bip39WordList wordList() {
return wordList;
}

@Override
public String entropyToMnemonic(byte[] entropy) {
throw new UnsupportedOperationException("entropyToMnemonic is not implemented yet");
}

@Override
public byte[] mnemonicToEntropy(String mnemonic) {
throw new UnsupportedOperationException("mnemonicToEntropy is not implemented yet");
}

@Override
public byte[] mnemonicToEntropy(List<String> words) {
throw new UnsupportedOperationException("mnemonicToEntropy is not implemented yet");
}

@Override
public ValidationResult validateMnemonic(String mnemonic) {
throw new UnsupportedOperationException("validateMnemonic is not implemented yet");
}

@Override
public ValidationResult validateMnemonic(List<String> words) {
throw new UnsupportedOperationException("validateMnemonic is not implemented yet");
}

@Override
public byte[] mnemonicToSeed(String mnemonic, String passphrase) {
throw new UnsupportedOperationException("mnemonicToSeed is not implemented yet");
}

@Override
public byte[] mnemonicToSeed(List<String> words, String passphrase) {
throw new UnsupportedOperationException("mnemonicToSeed is not implemented yet");
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/example/bip39/error/Bip39ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.bip39.error;

public enum Bip39ErrorCode {
ERR_ENTROPY_LENGTH,
ERR_INVALID_MNEMONIC_FORMAT,
ERR_INVALID_WORD_COUNT,
ERR_WORD_NOT_IN_LIST,
ERR_CHECKSUM_MISMATCH,
ERR_PBKDF2_FAILURE
}
27 changes: 27 additions & 0 deletions src/main/java/com/example/bip39/error/Bip39Exception.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.example.bip39.error;

import java.util.Objects;

public final class Bip39Exception extends RuntimeException {

private final Bip39ErrorCode errorCode;

public Bip39Exception(Bip39ErrorCode errorCode) {
super(errorCode.name());
this.errorCode = Objects.requireNonNull(errorCode, "errorCode must not be null");
}

public Bip39Exception(Bip39ErrorCode errorCode, String message) {
super(message);
this.errorCode = Objects.requireNonNull(errorCode, "errorCode must not be null");
}

public Bip39Exception(Bip39ErrorCode errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = Objects.requireNonNull(errorCode, "errorCode must not be null");
}

public Bip39ErrorCode getErrorCode() {
return errorCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.bip39.error;

import java.util.List;

public final class Bip39ValidationErrorOrder {

public static final List<Bip39ErrorCode> STRICT_VALIDATION_ORDER =
List.of(
Bip39ErrorCode.ERR_INVALID_MNEMONIC_FORMAT,
Bip39ErrorCode.ERR_INVALID_WORD_COUNT,
Bip39ErrorCode.ERR_WORD_NOT_IN_LIST,
Bip39ErrorCode.ERR_CHECKSUM_MISMATCH);

private Bip39ValidationErrorOrder() {}
}
29 changes: 29 additions & 0 deletions src/main/java/com/example/bip39/model/ValidationResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example.bip39.model;

import com.example.bip39.error.Bip39ErrorCode;

public record ValidationResult(
boolean ok,
Bip39ErrorCode errorCode,
String normalizedMnemonic,
Integer wordCount,
String invalidWord) {

public ValidationResult {
if (ok && errorCode != null) {
throw new IllegalArgumentException("Successful validation must not have an error code");
}
if (!ok && errorCode == null) {
throw new IllegalArgumentException("Failed validation must have an error code");
}
}

public static ValidationResult success(String normalizedMnemonic, Integer wordCount) {
return new ValidationResult(true, null, normalizedMnemonic, wordCount, null);
}

public static ValidationResult failure(
Bip39ErrorCode errorCode, String normalizedMnemonic, Integer wordCount, String invalidWord) {
return new ValidationResult(false, errorCode, normalizedMnemonic, wordCount, invalidWord);
}
}
37 changes: 37 additions & 0 deletions src/main/java/com/example/bip39/util/Bip39Constants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.example.bip39.util;

import java.util.List;
import java.util.Set;

public final class Bip39Constants {

public static final List<Integer> ALLOWED_ENTROPY_LENGTHS_BYTES = List.of(16, 20, 24, 28, 32);
public static final Set<Integer> ALLOWED_ENTROPY_LENGTHS_BYTES_SET =
Set.copyOf(ALLOWED_ENTROPY_LENGTHS_BYTES);
public static final int WORD_LIST_SIZE = 2048;
public static final int PBKDF2_ITERATIONS = 2048;
public static final int SEED_LEN_BYTES = 64;

private Bip39Constants() {}

public static boolean isAllowedEntropyLengthBytes(int entropyLengthBytes) {
return ALLOWED_ENTROPY_LENGTHS_BYTES_SET.contains(entropyLengthBytes);
}

public static int checksumLengthBits(int entropyLengthBytes) {
validateEntropyLengthBytes(entropyLengthBytes);
return entropyLengthBytes * 8 / 32;
}

public static int mnemonicWordCount(int entropyLengthBytes) {
validateEntropyLengthBytes(entropyLengthBytes);
int entropyLengthBits = entropyLengthBytes * 8;
return (entropyLengthBits + checksumLengthBits(entropyLengthBytes)) / 11;
}

public static void validateEntropyLengthBytes(int entropyLengthBytes) {
if (!isAllowedEntropyLengthBytes(entropyLengthBytes)) {
throw new IllegalArgumentException("Unsupported entropy length bytes: " + entropyLengthBytes);
}
}
}
43 changes: 43 additions & 0 deletions src/main/java/com/example/bip39/wordlist/Bip39WordList.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.example.bip39.wordlist;

import java.util.List;
import java.util.Map;
import java.util.Objects;

public final class Bip39WordList {

private final List<String> words;
private final Map<String, Integer> wordIndexes;

Bip39WordList(List<String> words, Map<String, Integer> wordIndexes) {
this.words = List.copyOf(words);
this.wordIndexes = Map.copyOf(wordIndexes);
}

public int size() {
return words.size();
}

public List<String> words() {
return words;
}

public String indexToWord(int index) {
Objects.checkIndex(index, words.size());
return words.get(index);
}

public boolean containsWord(String word) {
Objects.requireNonNull(word, "word must not be null");
return wordIndexes.containsKey(word);
}

public int wordToIndex(String word) {
Objects.requireNonNull(word, "word must not be null");
Integer index = wordIndexes.get(word);
if (index == null) {
throw new IllegalArgumentException("Word is not in the BIP-39 word list: " + word);
}
return index;
}
}
Loading
Loading