Skip to content

Commit 51e9a07

Browse files
authored
Merge pull request #8 from xt0x/feat/external-integration
Implement MnemonicFlow and classes for mnemonic processing
2 parents 1584916 + 2b03d5f commit 51e9a07

5 files changed

Lines changed: 259 additions & 0 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.example.bip39.integration;
2+
3+
@FunctionalInterface
4+
public interface Bip32SeedConsumer {
5+
6+
void accept(byte[] seed);
7+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.example.bip39.integration;
2+
3+
import com.example.bip39.error.Bip39ErrorCode;
4+
import java.util.EnumMap;
5+
import java.util.Map;
6+
import java.util.Objects;
7+
8+
public final class Bip39UiMessages {
9+
10+
private static final Map<Bip39ErrorCode, String> UI_MESSAGES = createUiMessages();
11+
12+
private Bip39UiMessages() {}
13+
14+
public static String messageFor(Bip39ErrorCode errorCode) {
15+
Objects.requireNonNull(errorCode, "errorCode must not be null");
16+
return UI_MESSAGES.get(errorCode);
17+
}
18+
19+
private static Map<Bip39ErrorCode, String> createUiMessages() {
20+
EnumMap<Bip39ErrorCode, String> messages = new EnumMap<>(Bip39ErrorCode.class);
21+
messages.put(
22+
Bip39ErrorCode.ERR_ENTROPY_LENGTH, "Entropy length must be 16, 20, 24, 28, or 32 bytes.");
23+
messages.put(
24+
Bip39ErrorCode.ERR_INVALID_MNEMONIC_FORMAT, "Enter words in normalized lowercase form.");
25+
messages.put(
26+
Bip39ErrorCode.ERR_INVALID_WORD_COUNT,
27+
"Mnemonic word count must be 12, 15, 18, 21, or 24.");
28+
messages.put(
29+
Bip39ErrorCode.ERR_WORD_NOT_IN_LIST,
30+
"Mnemonic contains a word outside the English BIP39 list.");
31+
messages.put(
32+
Bip39ErrorCode.ERR_CHECKSUM_MISMATCH,
33+
"Mnemonic checksum does not match the words provided.");
34+
messages.put(
35+
Bip39ErrorCode.ERR_PBKDF2_FAILURE, "Seed derivation is unavailable in this runtime.");
36+
return Map.copyOf(messages);
37+
}
38+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.example.bip39.integration;
2+
3+
import com.example.bip39.api.Bip39Service;
4+
import com.example.bip39.error.Bip39ErrorCode;
5+
import com.example.bip39.error.Bip39Exception;
6+
import com.example.bip39.model.ValidationResult;
7+
import com.example.bip39.normalize.MnemonicInputNormalizer;
8+
import java.util.Objects;
9+
10+
public final class MnemonicFlow {
11+
12+
private final Bip39Service bip39Service;
13+
14+
public MnemonicFlow(Bip39Service bip39Service) {
15+
this.bip39Service = Objects.requireNonNull(bip39Service, "bip39Service must not be null");
16+
}
17+
18+
public PreparedMnemonicInput prepareInput(String rawMnemonic) {
19+
Objects.requireNonNull(rawMnemonic, "rawMnemonic must not be null");
20+
return new PreparedMnemonicInput(
21+
rawMnemonic, MnemonicInputNormalizer.normalizeMnemonicInput(rawMnemonic));
22+
}
23+
24+
public ValidationResult validatePreparedInput(PreparedMnemonicInput preparedMnemonicInput) {
25+
Objects.requireNonNull(preparedMnemonicInput, "preparedMnemonicInput must not be null");
26+
return bip39Service.validateMnemonic(preparedMnemonicInput.normalizedMnemonic());
27+
}
28+
29+
public byte[] deriveSeed(PreparedMnemonicInput preparedMnemonicInput, String passphrase) {
30+
Objects.requireNonNull(preparedMnemonicInput, "preparedMnemonicInput must not be null");
31+
return bip39Service.mnemonicToSeed(
32+
preparedMnemonicInput.normalizedMnemonic(), requirePassphrase(passphrase));
33+
}
34+
35+
public byte[] validateThenDeriveSeed(
36+
PreparedMnemonicInput preparedMnemonicInput, String passphrase) {
37+
ValidationResult validationResult = validatePreparedInput(preparedMnemonicInput);
38+
if (!validationResult.ok()) {
39+
throw validationFailure(validationResult.errorCode());
40+
}
41+
return deriveSeed(preparedMnemonicInput, passphrase);
42+
}
43+
44+
public byte[] validateThenSendSeedToBip32(
45+
String rawMnemonic, String passphrase, Bip32SeedConsumer bip32SeedConsumer) {
46+
Objects.requireNonNull(bip32SeedConsumer, "bip32SeedConsumer must not be null");
47+
48+
PreparedMnemonicInput preparedMnemonicInput = prepareInput(rawMnemonic);
49+
byte[] seed = validateThenDeriveSeed(preparedMnemonicInput, passphrase);
50+
bip32SeedConsumer.accept(seed.clone());
51+
return seed;
52+
}
53+
54+
public String uiMessage(Bip39ErrorCode errorCode) {
55+
return Bip39UiMessages.messageFor(errorCode);
56+
}
57+
58+
private static String requirePassphrase(String passphrase) {
59+
return Objects.requireNonNull(passphrase, "passphrase must not be null");
60+
}
61+
62+
private static Bip39Exception validationFailure(Bip39ErrorCode errorCode) {
63+
return new Bip39Exception(errorCode, Bip39UiMessages.messageFor(errorCode));
64+
}
65+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.example.bip39.integration;
2+
3+
import java.util.Objects;
4+
5+
public record PreparedMnemonicInput(String originalInput, String normalizedMnemonic) {
6+
7+
public PreparedMnemonicInput {
8+
Objects.requireNonNull(originalInput, "originalInput must not be null");
9+
Objects.requireNonNull(normalizedMnemonic, "normalizedMnemonic must not be null");
10+
}
11+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package com.example.bip39.integration;
2+
3+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.assertFalse;
6+
import static org.junit.jupiter.api.Assertions.assertNotSame;
7+
import static org.junit.jupiter.api.Assertions.assertThrows;
8+
import static org.junit.jupiter.api.Assertions.assertTrue;
9+
10+
import com.example.bip39.api.Bip39Service;
11+
import com.example.bip39.api.DefaultBip39Service;
12+
import com.example.bip39.error.Bip39ErrorCode;
13+
import com.example.bip39.error.Bip39Exception;
14+
import com.example.bip39.model.ValidationResult;
15+
import java.util.HexFormat;
16+
import java.util.concurrent.atomic.AtomicBoolean;
17+
import java.util.concurrent.atomic.AtomicReference;
18+
import org.junit.jupiter.api.Test;
19+
20+
class MnemonicFlowTest {
21+
22+
private static final String RAW_ZERO_ENTROPY_MNEMONIC =
23+
" ABANDON\tabandon\nABANDON\rabandon abandon\tabandon\nabandon\rabandon"
24+
+ " abandon\tabandon\nABANDON\rABOUT ";
25+
26+
private static final String NORMALIZED_ZERO_ENTROPY_MNEMONIC =
27+
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon"
28+
+ " about";
29+
30+
private static final String ZERO_ENTROPY_SEED_WITH_TREZOR =
31+
"c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e5349553"
32+
+ "1f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04";
33+
34+
@Test
35+
void preparesUiInputWhilePreservingOriginalText() {
36+
MnemonicFlow mnemonicFlow = new MnemonicFlow(new DefaultBip39Service());
37+
38+
PreparedMnemonicInput preparedMnemonicInput =
39+
mnemonicFlow.prepareInput(RAW_ZERO_ENTROPY_MNEMONIC);
40+
41+
assertEquals(RAW_ZERO_ENTROPY_MNEMONIC, preparedMnemonicInput.originalInput());
42+
assertEquals(NORMALIZED_ZERO_ENTROPY_MNEMONIC, preparedMnemonicInput.normalizedMnemonic());
43+
}
44+
45+
@Test
46+
void validatesNormalizedInputSeparatelyFromDerivation() {
47+
MnemonicFlow mnemonicFlow = new MnemonicFlow(new DefaultBip39Service());
48+
49+
ValidationResult validationResult =
50+
mnemonicFlow.validatePreparedInput(mnemonicFlow.prepareInput(RAW_ZERO_ENTROPY_MNEMONIC));
51+
52+
assertTrue(validationResult.ok());
53+
assertEquals(NORMALIZED_ZERO_ENTROPY_MNEMONIC, validationResult.normalizedMnemonic());
54+
}
55+
56+
@Test
57+
void validatesBeforeDerivingSeedWhenRequested() {
58+
RecordingBip39Service bip39Service = new RecordingBip39Service();
59+
MnemonicFlow mnemonicFlow = new MnemonicFlow(bip39Service);
60+
PreparedMnemonicInput preparedMnemonicInput =
61+
new PreparedMnemonicInput("abandon typo", "abandon typo");
62+
63+
Bip39Exception exception =
64+
assertThrows(
65+
Bip39Exception.class,
66+
() -> mnemonicFlow.validateThenDeriveSeed(preparedMnemonicInput, "TREZOR"));
67+
68+
assertEquals(Bip39ErrorCode.ERR_INVALID_WORD_COUNT, exception.getErrorCode());
69+
assertFalse(bip39Service.mnemonicToSeedCalled.get());
70+
}
71+
72+
@Test
73+
void forwardsValidatedSeedToBip32Consumer() {
74+
MnemonicFlow mnemonicFlow = new MnemonicFlow(new DefaultBip39Service());
75+
AtomicReference<byte[]> capturedSeed = new AtomicReference<>();
76+
77+
byte[] returnedSeed =
78+
mnemonicFlow.validateThenSendSeedToBip32(
79+
RAW_ZERO_ENTROPY_MNEMONIC, "TREZOR", capturedSeed::set);
80+
81+
assertEquals(ZERO_ENTROPY_SEED_WITH_TREZOR, HexFormat.of().formatHex(returnedSeed));
82+
assertArrayEquals(returnedSeed, capturedSeed.get());
83+
assertNotSame(returnedSeed, capturedSeed.get());
84+
}
85+
86+
@Test
87+
void mapsErrorCodesToUiMessages() {
88+
MnemonicFlow mnemonicFlow = new MnemonicFlow(new DefaultBip39Service());
89+
90+
assertEquals(
91+
"Mnemonic contains a word outside the English BIP39 list.",
92+
mnemonicFlow.uiMessage(Bip39ErrorCode.ERR_WORD_NOT_IN_LIST));
93+
assertEquals(
94+
"Mnemonic checksum does not match the words provided.",
95+
mnemonicFlow.uiMessage(Bip39ErrorCode.ERR_CHECKSUM_MISMATCH));
96+
}
97+
98+
private static final class RecordingBip39Service implements Bip39Service {
99+
100+
private final AtomicBoolean mnemonicToSeedCalled = new AtomicBoolean(false);
101+
102+
@Override
103+
public String entropyToMnemonic(byte[] entropy) {
104+
throw new UnsupportedOperationException();
105+
}
106+
107+
@Override
108+
public byte[] mnemonicToEntropy(String mnemonic) {
109+
throw new UnsupportedOperationException();
110+
}
111+
112+
@Override
113+
public byte[] mnemonicToEntropy(java.util.List<String> words) {
114+
throw new UnsupportedOperationException();
115+
}
116+
117+
@Override
118+
public ValidationResult validateMnemonic(String mnemonic) {
119+
return ValidationResult.failure(Bip39ErrorCode.ERR_INVALID_WORD_COUNT, mnemonic, 2, null);
120+
}
121+
122+
@Override
123+
public ValidationResult validateMnemonic(java.util.List<String> words) {
124+
throw new UnsupportedOperationException();
125+
}
126+
127+
@Override
128+
public byte[] mnemonicToSeed(String mnemonic, String passphrase) {
129+
mnemonicToSeedCalled.set(true);
130+
return new byte[64];
131+
}
132+
133+
@Override
134+
public byte[] mnemonicToSeed(java.util.List<String> words, String passphrase) {
135+
throw new UnsupportedOperationException();
136+
}
137+
}
138+
}

0 commit comments

Comments
 (0)