Skip to content

Commit

Permalink
More strict BIP39 checking
Browse files Browse the repository at this point in the history
  • Loading branch information
mikera committed Nov 30, 2024
1 parent 49cc762 commit 4bbf4bf
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 24 deletions.
49 changes: 43 additions & 6 deletions convex-core/src/main/java/convex/core/crypto/BIP39.java
Original file line number Diff line number Diff line change
Expand Up @@ -273,14 +273,20 @@ public static Blob seedToEd25519Seed(Blob seed) {
}

/**
* Tests if the string is a valid mnemonic phrase, returns null if no problem
* Tests if the string is a valid BIP39 mnemonic phrase, returns null if no problem
* @param s String to be tested as a mnemonic phrase
* @return String containing reason that mnemonic is not valid, or null if OK
*/
public static String checkMnemonic(String s) {
List<String> words=getWords(s);
if (words.size()<MIN_WORDS) return "Inadqeuate number of words in BIP39 mnemonic (at least "+MIN_WORDS+" recommended)";
return checkWords(words);

String err= checkWords(words);
if (err!=null) return "Not in word list: "+err;

if (!s.equals(normaliseFormat(s))) return "String not normalised";

return null;
}

/**
Expand All @@ -290,7 +296,6 @@ public static String checkMnemonic(String s) {
* @return Blob containing BIP39 seed (64 bytes)
*/
public static Blob getSeed(String mnemonic, String passphrase) {
mnemonic=normaliseFormat(mnemonic);
mnemonic=Normalizer.normalize(mnemonic, Normalizer.Form.NFKD);
char[] normalisedMnemonic= mnemonic.toCharArray();
return getSeedInternal(normalisedMnemonic,passphrase);
Expand Down Expand Up @@ -340,25 +345,56 @@ public static List<String> createWords(SecureRandom r, int n) {
return createWords(bs,n);
}

public static List<String> createWords(byte[] entropy, int n) {
public static List<String> createWordsAddingChecksum(byte[] entropy, int n) {
int CS=n/3; // number of checksum bits
int ENT=CS*32;
Hash checkHash=Hashing.sha256(entropy);
int checkSum=Utils.extractBits(checkHash.getBytes(), CS, 256-CS); // BIP39 checksum

int blen=((CS+ENT)/8)+1; // enough space for entropy plus checksum
int blen=((CS+ENT+7)/8); // enough space for entropy plus checksum
byte[] bs=new byte[blen];
System.arraycopy(entropy, 0, bs, 0, ENT/8);
Utils.setBits(bs, CS, (blen*8)-(ENT+CS),checkSum);

return createWords(bs,n);
}

public static List<String> createWords(byte[] material, int n) {
int mbits=material.length*8;
ArrayList<String> al=new ArrayList<>(n);
for (int i=0; i<n; i++) {
int ix=Utils.extractBits(bs, BITS_PER_WORD, (blen*8) - (i+1)*BITS_PER_WORD);
int ix=Utils.extractBits(material, BITS_PER_WORD, mbits - (i+1)*BITS_PER_WORD);
String word=wordlist[ix];
al.add(word);
}
return al;
}

/**
* Gets bytes containing the entropy and checksum used to create the given words
* @param mnemonic
* @return byte array of sufficient size, or null if not valid BIP39 words
*/
public static byte[] mnemonicToBytes(String mnemonic) {
List<String> words=getWords(mnemonic);
int n=words.size();
if ((n<MIN_WORDS)||(n>24)) return null;

int CS=n/3;
if ((CS*3!=n)) return null; // must be a multiple of 3 for valid BIP39
int ENT=CS*32;

int blen=((CS+ENT+7)/8); // enough space for entropy plus checksum
byte[] bs=new byte[blen];

for (int i=0; i<n; i++) {
String w=words.get(i);
Integer ix=LOOKUP.get(w);
if (ix==null) return null;
Utils.setBits(bs, BITS_PER_WORD, (blen*8) - (i+1)*BITS_PER_WORD, ix);
}
return bs;
}

/**
* Gets the individual words from a mnemonic String. Will trim and normalise whitespace
Expand Down Expand Up @@ -399,6 +435,7 @@ public static String normaliseAll(String s) {
String ext=extendWord(w);
if (ext!=null) {
words.set(i, ext);
continue;
}

words.set(i, w.toUpperCase()); // An unexpected word, highlight in uppercase
Expand Down
34 changes: 18 additions & 16 deletions convex-core/src/test/java/convex/core/crypto/BIP39Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,30 @@
import convex.core.data.Blob;

public class BIP39Test {

@Test public void testWordList() {
assertEquals(2048,BIP39.wordlist.length);
}


@Test
public void testFromEntropy() {
// Test vectors from https://github.com/trezor/python-mnemonic/blob/master/vectors.json
{
byte[] ent=Blob.fromHex("00000000000000000000000000000000").getBytes();
String ph=BIP39.mnemonic(BIP39.createWords(ent, 12));
String ph=BIP39.mnemonic(BIP39.createWordsAddingChecksum(ent, 12));
assertEquals("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",ph);
}

{
byte[] ent=Blob.fromHex("ffffffffffffffffffffffffffffffff").getBytes();
String ph=BIP39.mnemonic(BIP39.createWords(ent, 12));
String ph=BIP39.mnemonic(BIP39.createWordsAddingChecksum(ent, 12));
assertEquals("zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong",ph);
}

{
byte[] ent=Blob.fromHex("68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c").getBytes();
String ph=BIP39.mnemonic(BIP39.createWords(ent, 24));
String ph=BIP39.mnemonic(BIP39.createWordsAddingChecksum(ent, 24));
assertEquals("hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length",ph);
}


}

@Test
Expand All @@ -53,6 +49,9 @@ public void testExtendWord() {
assertEquals("zoo",BIP39.extendWord(" zoo "));
assertEquals("list",BIP39.extendWord("list"));
assertEquals("capital",BIP39.extendWord("capi"));
assertNull(BIP39.extendWord(""));
assertNull(BIP39.extendWord("z"));
assertNull(BIP39.extendWord(" zo"));
}

@Test
Expand All @@ -78,9 +77,10 @@ public void testExample15() throws NoSuchAlgorithmException, InvalidKeySpecExcep
assertEquals(exSeed,seed.toHexString());
assertEquals(BIP39.SEED_LENGTH,seed.count());

// Different string equals different seed
String s2=s1.replaceAll(" " , " ");
assertNotEquals(s1,s2);
assertEquals(exSeed,BIP39.getSeed(s2,"").toHexString());
assertNotEquals(exSeed,BIP39.getSeed(s2,"").toHexString());
}

@ParameterizedTest
Expand All @@ -104,8 +104,7 @@ public void doMnemonicTest(String m) {
@Test public void testNewlyGenerated() {
doValidStringTest(BIP39.createSecureMnemonic(3));
doValidStringTest(BIP39.createSecureMnemonic(15));
doValidStringTest(" "+BIP39.createSecureMnemonic(12));
doValidStringTest(BIP39.createSecureMnemonic(24)+"\t");
doValidStringTest(BIP39.createSecureMnemonic(24));
doValidStringTest(BIP39.mnemonic(BIP39.createWords(new InsecureRandom(4), 3)));
doValidStringTest(BIP39.mnemonic(BIP39.createWords(new InsecureRandom(16), 12)));

Expand All @@ -116,31 +115,34 @@ public void doMnemonicTest(String m) {

@Test
public void testValidStrings() {
doValidStringTest("behind emotion squeeze"); // insufficient words
doValidStringTest("behinD Emotion SQUEEZE"); // insufficient words
doValidStringTest("behind emotion squeeze");
}

private void doValidStringTest(String m) {
assertNull(BIP39.checkMnemonic(m));
String PP="pass";
List<String> words=BIP39.getWords(m);
int n=words.size();
try {
Blob seed=BIP39.getSeed(words, PP);
assertEquals(BIP39.SEED_LENGTH,seed.count());

// Wrong passphrase => different seed
assertNotEquals(seed,BIP39.getSeed(words, "badpass"));

// with extra whitespace is OK
assertEquals(seed,BIP39.getSeed(" \t "+m, PP));

AKeyPair kp=BIP39.seedToKeyPair(seed);
assertNotNull(kp);

} catch (Exception e) {
fail("Enexpected Exception "+e);
fail("Unexpected Exception "+e);
}

// Tests for round trips to entropy
byte[] bs=BIP39.mnemonicToBytes(m);
List<String> rwords=BIP39.createWords(bs, n);
String rm=BIP39.mnemonic(rwords);

assertEquals(m,rm);
}

@Test
Expand Down
3 changes: 1 addition & 2 deletions convex-gui/src/main/java/convex/gui/keys/KeyGenPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ 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);

Expand All @@ -100,7 +99,7 @@ private void generateBIP39Seed() {
}

try {
Blob bipSeed=BIP39.getSeed(words,p);
Blob bipSeed=BIP39.getSeed(s, p);
seedArea.setText(bipSeed.toHexString());
deriveSeed();
} catch (Exception ex) {
Expand Down

0 comments on commit 4bbf4bf

Please sign in to comment.