From a4f264deb468d5e9b3618e631fb75cfa4f604269 Mon Sep 17 00:00:00 2001 From: mikera Date: Sat, 30 Nov 2024 17:40:17 +0000 Subject: [PATCH] Extra BIP39 checksum logic and tests --- .../main/java/convex/core/crypto/BIP39.java | 37 ++++++++++++++++++- .../java/convex/core/crypto/BIP39Test.java | 19 ++++++++-- .../java/convex/core/data/EncodingTest.java | 3 ++ .../java/convex/core/data/SyntaxTest.java | 10 +++++ .../java/convex/gui/keys/KeyGenPanel.java | 17 ++++----- 5 files changed, 73 insertions(+), 13 deletions(-) diff --git a/convex-core/src/main/java/convex/core/crypto/BIP39.java b/convex-core/src/main/java/convex/core/crypto/BIP39.java index 5e2f72f0b..925ccefdd 100644 --- a/convex-core/src/main/java/convex/core/crypto/BIP39.java +++ b/convex-core/src/main/java/convex/core/crypto/BIP39.java @@ -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; @@ -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 @@ -342,7 +348,7 @@ public static List 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 createWordsAddingChecksum(byte[] entropy, int n) { @@ -359,6 +365,26 @@ public static List 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 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 createWords(byte[] material, int n) { int mbits=material.length*8; ArrayList al=new ArrayList<>(n); @@ -377,6 +403,15 @@ public static List createWords(byte[] material, int n) { */ public static byte[] mnemonicToBytes(String mnemonic) { List 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 words) { int n=words.size(); if ((n24)) return null; diff --git a/convex-core/src/test/java/convex/core/crypto/BIP39Test.java b/convex-core/src/test/java/convex/core/crypto/BIP39Test.java index 2577bf62a..6a863cb8a 100644 --- a/convex-core/src/test/java/convex/core/crypto/BIP39Test.java +++ b/convex-core/src/test/java/convex/core/crypto/BIP39Test.java @@ -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; @@ -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); } } @@ -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))); @@ -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 words=BIP39.getWords(m); int n=words.size(); @@ -142,6 +152,9 @@ private void doValidStringTest(String m) { List rwords=BIP39.createWords(bs, n); String rm=BIP39.mnemonic(rwords); + + assertNull(BIP39.checkMnemonic(m),()->"For string: "+m); + assertEquals(m,rm); } diff --git a/convex-core/src/test/java/convex/core/data/EncodingTest.java b/convex-core/src/test/java/convex/core/data/EncodingTest.java index 954e47abe..3915ff2d5 100644 --- a/convex-core/src/test/java/convex/core/data/EncodingTest.java +++ b/convex-core/src/test/java/convex/core/data/EncodingTest.java @@ -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 v1=Vectors.of(1,Samples.NON_EMBEDDED_STRING,Samples.NON_EMBEDDED_STRING,Samples.INT_VECTOR_23); doMultiEncodingTest(v1); diff --git a/convex-core/src/test/java/convex/core/data/SyntaxTest.java b/convex-core/src/test/java/convex/core/data/SyntaxTest.java index a3602f6a0..22b0c0087 100644 --- a/convex-core/src/test/java/convex/core/data/SyntaxTest.java +++ b/convex-core/src/test/java/convex/core/data/SyntaxTest.java @@ -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 diff --git a/convex-gui/src/main/java/convex/gui/keys/KeyGenPanel.java b/convex-gui/src/main/java/convex/gui/keys/KeyGenPanel.java index 4f9accd55..e06f2ffaa 100644 --- a/convex-gui/src/main/java/convex/gui/keys/KeyGenPanel.java +++ b/convex-gui/src/main/java/convex/gui/keys/KeyGenPanel.java @@ -123,20 +123,22 @@ private String checkWarnings(String s, String p) { String badWord=BIP39.checkWords(words); int numWords=words.size(); - if (numWords24) { 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 { @@ -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; }