Skip to content

Commit

Permalink
Extra BIP39 checksum logic and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mikera committed Nov 30, 2024
1 parent 4bbf4bf commit a4f264d
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 13 deletions.
37 changes: 36 additions & 1 deletion convex-core/src/main/java/convex/core/crypto/BIP39.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

import org.bouncycastle.util.Arrays;

import convex.core.data.Blob;
import convex.core.data.Hash;
import convex.core.util.Utils;
Expand Down Expand Up @@ -286,9 +288,13 @@ public static String checkMnemonic(String s) {

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

if (!checkSum(s)) return "Invalid checksum";

return null;
}



/**
* Gets a BIP39 seed given a mnemonic and passphrase
* @param mnemonic Mnemonic words
Expand Down Expand Up @@ -342,7 +348,7 @@ public static List<String> createWords(SecureRandom r, int n) {
byte[] bs=new byte[ENT/8]; // enough space for entropy
r.nextBytes(bs);

return createWords(bs,n);
return createWordsAddingChecksum(bs,n);
}

public static List<String> createWordsAddingChecksum(byte[] entropy, int n) {
Expand All @@ -359,6 +365,26 @@ public static List<String> createWordsAddingChecksum(byte[] entropy, int n) {
return createWords(bs,n);
}

/**
* Check if BIP39 checksum is correct
* @param s
* @return True if BIP39 checksum is valid
*/
public static boolean checkSum(String mnemonic) {
List<String> words=getWords(mnemonic);
int n=words.size();
byte[] bs=mnemonicToBytes(words);
if (bs==null) return false;

int CS=n/3; // number of checksum bits
int ENT=CS*32;
Hash checkHash=Hashing.sha256(Arrays.copyOf(bs, ENT/8));
int checkSum=Utils.extractBits(checkHash.getBytes(), CS, 256-CS); // BIP39 checksum

int storedSum=Utils.extractBits(bs, CS, (bs.length*8)-(ENT+CS));
return checkSum==storedSum;
}

public static List<String> createWords(byte[] material, int n) {
int mbits=material.length*8;
ArrayList<String> al=new ArrayList<>(n);
Expand All @@ -377,6 +403,15 @@ public static List<String> createWords(byte[] material, int n) {
*/
public static byte[] mnemonicToBytes(String mnemonic) {
List<String> words=getWords(mnemonic);
return mnemonicToBytes(words);
}

/**
* 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(List<String> words) {
int n=words.size();
if ((n<MIN_WORDS)||(n>24)) return null;

Expand Down
19 changes: 16 additions & 3 deletions convex-core/src/test/java/convex/core/crypto/BIP39Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.security.NoSuchAlgorithmException;
Expand All @@ -28,18 +29,22 @@ public void testFromEntropy() {
byte[] ent=Blob.fromHex("00000000000000000000000000000000").getBytes();
String ph=BIP39.mnemonic(BIP39.createWordsAddingChecksum(ent, 12));
assertEquals("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",ph);
Blob b=BIP39.getSeed(ph, "TREZOR");
assertEquals("c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04",b.toHexString());
}

{
byte[] ent=Blob.fromHex("ffffffffffffffffffffffffffffffff").getBytes();
String ph=BIP39.mnemonic(BIP39.createWordsAddingChecksum(ent, 12));
assertEquals("zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong",ph);
assertTrue(BIP39.checkSum(ph)); // should be valid checksum
}

{
byte[] ent=Blob.fromHex("68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c").getBytes();
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);
doMnemonicTest(ph);
}
}

Expand Down Expand Up @@ -102,9 +107,10 @@ public void doMnemonicTest(String m) {
}

@Test public void testNewlyGenerated() {
doValidStringTest(BIP39.createSecureMnemonic(3));
doValidStringTest(BIP39.createSecureMnemonic(12));
doValidStringTest(BIP39.createSecureMnemonic(15));
doValidStringTest(BIP39.createSecureMnemonic(24));
doValidStringTest(BIP39.createSecureMnemonic(3));
doValidStringTest(BIP39.mnemonic(BIP39.createWords(new InsecureRandom(4), 3)));
doValidStringTest(BIP39.mnemonic(BIP39.createWords(new InsecureRandom(16), 12)));

Expand All @@ -115,11 +121,15 @@ public void doMnemonicTest(String m) {

@Test
public void testValidStrings() {
doValidStringTest("behind emotion squeeze");
doValidStringTest("double liar property");

// Another example from https://github.com/trezor/python-mnemonic/blob/master/vectors.json
doValidStringTest("legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title");
}

private void doValidStringTest(String m) {
assertNull(BIP39.checkMnemonic(m));
assertTrue(BIP39.checkSum(m));

String PP="pass";
List<String> words=BIP39.getWords(m);
int n=words.size();
Expand All @@ -142,6 +152,9 @@ private void doValidStringTest(String m) {
List<String> rwords=BIP39.createWords(bs, n);
String rm=BIP39.mnemonic(rwords);


assertNull(BIP39.checkMnemonic(m),()->"For string: "+m);

assertEquals(m,rm);
}

Expand Down
3 changes: 3 additions & 0 deletions convex-core/src/test/java/convex/core/data/EncodingTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,9 @@ public void testFailedMissingEncoding() throws BadFormatException {
doMultiEncodingTest(Samples.NON_EMBEDDED_STRING);
doMultiEncodingTest(Vectors.of(1,2,3));

doMultiEncodingTest(Syntax.create(Address.create(32)));


// Two non-embedded identical children
AVector<ACell> v1=Vectors.of(1,Samples.NON_EMBEDDED_STRING,Samples.NON_EMBEDDED_STRING,Samples.INT_VECTOR_23);
doMultiEncodingTest(v1);
Expand Down
10 changes: 10 additions & 0 deletions convex-core/src/test/java/convex/core/data/SyntaxTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ public class SyntaxTest {
assertThrows(BadFormatException.class,()->Format.read("88008200"));
}

@Test public void testSyntaxExamples() {
Syntax s1= Syntax.create(Address.create(32));

doSyntaxTest(s1);
}

private void doSyntaxTest(Syntax s) {
ObjectsTest.doAnyValueTests(s);
}

/**
* A Syntax wrapped in another Syntax should not be a valid encoding
* @throws BadFormatException On format error
Expand Down
17 changes: 8 additions & 9 deletions convex-gui/src/main/java/convex/gui/keys/KeyGenPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,20 +123,22 @@ private String checkWarnings(String s, String p) {
String badWord=BIP39.checkWords(words);

int numWords=words.size();
if (numWords<BIP39.MIN_WORDS) {
if (numWords<12) {
warn+="Only "+numWords+" words. ";
} else if ((numWords!=((numWords/3)*3)) || numWords>24) {
warn+="Unusual number of words ("+numWords+"). ";

}

if (badWord!=null) {
} else if (badWord!=null) {
if (BIP39.extendWord(badWord)!=null) {
warn += "Should normalise abbreviated word: "+badWord+". ";
} else {
warn +="Not in standard word list: "+badWord+". ";
}
} else if (!s.equals(BIP39.normaliseFormat(s))) {
warn+="Not normalised! ";
} else if (!BIP39.checkSum(s)) {
warn+="Invalid checksum! ";
}

if (p.isBlank()) {
warn+="Passphrase is blank! ";
} else {
Expand All @@ -149,10 +151,7 @@ private String checkWarnings(String s, String p) {
warn+="Moderate passphrase. ";
}
}

if (!s.equals(BIP39.normaliseFormat(s))) {
warn+="Not normalised! ";
}

return warn;
}

Expand Down

0 comments on commit a4f264d

Please sign in to comment.