Skip to content

Commit

Permalink
Improve BIP39 generation and warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
mikera committed Nov 30, 2024
1 parent 74e8bc9 commit 49cc762
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 38 deletions.
50 changes: 38 additions & 12 deletions convex-core/src/main/java/convex/core/crypto/BIP39.java
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ public class BIP39 {

public static final int BITS_PER_WORD=11;

public static final String DEMO_PHRASE="sing bomb stay manual powder hard north mixture sausage lunch retreat desert";
public static final String DEMO_PASS="hello1234567890ZZ";

/**
* Map of words to integer values
*/
Expand Down Expand Up @@ -244,14 +247,7 @@ public class BIP39 {
* @return Blob containing BIP39 seed (64 bytes)
*/
public static Blob getSeed(List<String> words, String passphrase) throws NoSuchAlgorithmException, InvalidKeySpecException {
if (passphrase==null) passphrase="";

// Normalise words and convert to char array
String joined=Utils.joinStrings(words, " ");
joined=Normalizer.normalize(joined, Normalizer.Form.NFKD);
char[] pass= joined.toCharArray();

return getSeedInternal(pass,passphrase);
return getSeed(mnemonic(words),passphrase);
}

public static AKeyPair seedToKeyPair(Blob seed) {
Expand Down Expand Up @@ -365,7 +361,7 @@ public static List<String> createWords(byte[] entropy, int n) {
}

/**
* Gets the individual words from a mnemonic String. Will trim and normalise whitespace, convert to lowercase
* Gets the individual words from a mnemonic String. Will trim and normalise whitespace
* @param mnemonic Mnemonic String
* @return List of words
*/
Expand All @@ -376,9 +372,7 @@ public static List<String> getWords(String mnemonic) {
ArrayList<String> al=new ArrayList<>();
for (int i=0; i<ss.length; i++) {
String w=ss[i].trim();

if (!w.isBlank()) {
w=w.toLowerCase();
al.add(w);
}
}
Expand All @@ -390,10 +384,33 @@ public static String normaliseFormat(String s) {
s=s.toLowerCase();
return s;
}

public static String normaliseAll(String s) {
// to lowercase and standard whitespace
s=normaliseFormat(s);

List<String> words=getWords(s);

int n=words.size();
for (int i=0; i<n; i++) {
String w=words.get(i);
if (LOOKUP.containsKey(w)) continue; // legit word, continue

String ext=extendWord(w);
if (ext!=null) {
words.set(i, ext);
}

words.set(i, w.toUpperCase()); // An unexpected word, highlight in uppercase
}

String result = mnemonic(words);
return result;
}

/**
* Create a mnemonic String from a list of words, separated by spaces
* @param words List of words for mnemonic
* @param words List of words for mnemonic
* @return Combined mnemonic string
*/
public static String mnemonic(List<String> words) {
Expand All @@ -417,5 +434,14 @@ public static String checkWords(List<String> words) {
}
return null;
}

/**
* Extends an abbreviated form of a BIP39 word to a full word e.g. 'SHAL' => 'shallow'
* @param abbr
* @return
*/
public static String extendWord(String abbr) {
return ABBR.get(abbr.trim().toLowerCase());
}

}
8 changes: 8 additions & 0 deletions convex-core/src/test/java/convex/core/crypto/BIP39Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ public void testFromEntropy() {

}

@Test
public void testExtendWord() {
assertEquals("shallow",BIP39.extendWord("SHAL"));
assertEquals("zoo",BIP39.extendWord(" zoo "));
assertEquals("list",BIP39.extendWord("list"));
assertEquals("capital",BIP39.extendWord("capi"));
}

@Test
public void testSeed() throws NoSuchAlgorithmException, InvalidKeySpecException {
List<String> tw1=List.of("blue claw trip feature street glue element derive dentist rose daring cash".split(" "));
Expand Down
68 changes: 42 additions & 26 deletions convex-gui/src/main/java/convex/gui/keys/KeyGenPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,42 @@ private void updatePass() {
private void generateBIP39Seed() {
String s = mnemonicArea.getText();
String p = new String(passArea.getPassword());
List<String> words=BIP39.getWords(s);

String warn=checkWarnings(s,p);

if (warn.isBlank()) {
warningArea.setForeground(Color.GREEN);
warningArea.setText("OK: Reasonable mnemonic and passphrase");
} else {
warningArea.setForeground(Color.ORANGE);
warningArea.setText("WARNING: "+warn);
}

try {
Blob bipSeed=BIP39.getSeed(words,p);
seedArea.setText(bipSeed.toHexString());
deriveSeed();
} catch (Exception ex) {
String pks = "<mnemonic not valid>";
if (s.isBlank()) pks = "<enter valid private key or mnemonic>";
masterKeyArea.setText(pks);
warningArea.setText("");
derivedKeyArea.setText(pks);
privateKeyArea.setText(pks);
}
}

private String checkWarnings(String s, String p) {
String warn ="";

// Check for demo phrases
if (s.equals(BIP39.DEMO_PHRASE)) warn+= "Demo mnemonic (for testing only). ";
if (p.equals(BIP39.DEMO_PASS)) warn+= "Demo passphrase (for testing only). ";

List<String> words=BIP39.getWords(s);
String badWord=BIP39.checkWords(words);

String warn="";
int numWords=words.size();
if (numWords<BIP39.MIN_WORDS) {
warn+="Only "+numWords+" words. ";
Expand All @@ -100,7 +132,11 @@ private void generateBIP39Seed() {
}

if (badWord!=null) {
warn +="Not in standard word list: "+badWord+". ";
if (BIP39.extendWord(badWord)!=null) {
warn += "Should normalise abbreviated word: "+badWord+". ";
} else {
warn +="Not in standard word list: "+badWord+". ";
}
}
if (p.isBlank()) {
warn+="Passphrase is blank! ";
Expand All @@ -118,29 +154,9 @@ private void generateBIP39Seed() {
if (!s.equals(BIP39.normaliseFormat(s))) {
warn+="Not normalised! ";
}

if (warn.isBlank()) {
warningArea.setForeground(Color.GREEN);
warningArea.setText("OK: Reasonable mnemonic and passphrase");
} else {
warningArea.setForeground(Color.ORANGE);
warningArea.setText("WARNING: "+warn);
}

try {
Blob bipSeed=BIP39.getSeed(words,p);
seedArea.setText(bipSeed.toHexString());
deriveSeed();
} catch (Exception ex) {
String pks = "<mnemonic not valid>";
if (s.isBlank()) pks = "<enter valid private key or mnemonic>";
masterKeyArea.setText(pks);
warningArea.setText("");
derivedKeyArea.setText(pks);
privateKeyArea.setText(pks);
}
return warn;
}

private void updatePath() {
try {
String path=derivationArea.getText();
Expand Down Expand Up @@ -259,7 +275,7 @@ public KeyGenPanel(PeerGUI manager) {
add(formPanel, BorderLayout.CENTER);

{ // Mnemonic entry box
addLabel("Mnemonic Phrase","BIP39 Mnemonic phrase. These should be random words from the BIP39 standard word list.");
addLabel("Mnemonic Phrase","BIP39 Mnemonic phrase. These should be 12, 15, 18, 21 or 24 random words from the BIP39 standard word list.");
mnemonicArea = makeTextArea();
mnemonicArea.setWrapStyleWord(true);
mnemonicArea.setLineWrap(true);
Expand Down Expand Up @@ -410,7 +426,7 @@ public KeyGenPanel(PeerGUI manager) {
{ // Button to Normalise Mnemonic string
JButton btnNormalise = new ActionButton("Normalise Mnemonic",0xf0ff,e -> {
String s=mnemonicArea.getText();
String s2=BIP39.normaliseFormat(s);
String s2=BIP39.normaliseAll(s);
mnemonicArea.setText(s2);
updateMnemonic();
});
Expand Down

0 comments on commit 49cc762

Please sign in to comment.