diff --git a/convex-cli/src/main/java/convex/cli/key/KeyGenerate.java b/convex-cli/src/main/java/convex/cli/key/KeyGenerate.java index 2039cd2e9..d8712d0d4 100644 --- a/convex-cli/src/main/java/convex/cli/key/KeyGenerate.java +++ b/convex-cli/src/main/java/convex/cli/key/KeyGenerate.java @@ -1,6 +1,5 @@ package convex.cli.key; -import java.security.GeneralSecurityException; import java.security.KeyStore; import java.util.Arrays; @@ -42,29 +41,25 @@ public class KeyGenerate extends AKeyCommand { private String passphrase; private AKeyPair generateKeyPair() { - try { - if (bip39) { - String mnemonic=BIP39.createSecureMnemonic(12); - cli().println(mnemonic); - if (passphrase==null) { - if (cli().isInteractive()) { - passphrase=new String(cli().readPassword("Enter BIP39 passphrase: ")); - } else { - cli().paranoia("Passphrase must be explicity provided"); - passphrase=""; - } - } - if (passphrase.isBlank()) { - cli().paranoia("Cannot use an empty BIP39 passphrase for secure key generation"); + if (bip39) { + String mnemonic=BIP39.createSecureMnemonic(12); + cli().println(mnemonic); + if (passphrase==null) { + if (cli().isInteractive()) { + passphrase=new String(cli().readPassword("Enter BIP39 passphrase: ")); + } else { + cli().paranoia("Passphrase must be explicity provided"); + passphrase=""; } - Blob bipseed = BIP39.getSeed(mnemonic, passphrase); - AKeyPair result= BIP39.seedToKeyPair(bipseed); - return result; - } else { - return AKeyPair.generate(); } - } catch (GeneralSecurityException e) { - throw Utils.sneakyThrow(e); + if (passphrase.isBlank()) { + cli().paranoia("Cannot use an empty BIP39 passphrase for secure key generation"); + } + Blob bipseed = BIP39.getSeed(mnemonic, passphrase); + AKeyPair result= BIP39.seedToKeyPair(bipseed); + return result; + } else { + return AKeyPair.generate(); } } 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 13d950fa1..f4598c063 100644 --- a/convex-core/src/main/java/convex/core/crypto/BIP39.java +++ b/convex-core/src/main/java/convex/core/crypto/BIP39.java @@ -259,7 +259,7 @@ public static Blob seedToEd25519Seed(Blob seed) { throw new IllegalArgumentException("Expected "+SEED_LENGTH+ " byte BIP39 seed but was: "+n); } Blob master=SLIP10.getMaster(seed); - return SLIP10.deriveKey(master); + return master.slice(0, 32); } /** @@ -281,30 +281,32 @@ public static String checkMnemonic(String s) { * @throws NoSuchAlgorithmException * @throws InvalidKeySpecException */ - public static Blob getSeed(String mnemonic, String passphrase) throws NoSuchAlgorithmException, InvalidKeySpecException { - mnemonic=normalise(mnemonic); - mnemonic=Normalizer.normalize(mnemonic, Normalizer.Form.NFKD); - char[] normalisedMnemonic= mnemonic.toCharArray(); - return getSeedInternal(normalisedMnemonic,passphrase); + public static Blob getSeed(String mnemonic, String passphrase) { + mnemonic=normalise(mnemonic); + mnemonic=Normalizer.normalize(mnemonic, Normalizer.Form.NFKD); + char[] normalisedMnemonic= mnemonic.toCharArray(); + return getSeedInternal(normalisedMnemonic,passphrase); } - private static Blob getSeedInternal(char[] normalisedMnemonic, String passphrase) throws NoSuchAlgorithmException, InvalidKeySpecException { - // Normalise passphrase and convert to byte array - passphrase=Normalizer.normalize(passphrase, Normalizer.Form.NFKD); - byte[] salt = ("mnemonic"+passphrase).getBytes(StandardCharsets.UTF_8); - - // Generate seed - SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); - KeySpec keyspec = new PBEKeySpec(normalisedMnemonic, salt, 2048, SEED_LENGTH * 8); - Key key = factory.generateSecret(keyspec); - - // Wrap result as Blob - byte[] bs = key.getEncoded(); - return Blob.wrap(bs); + private static Blob getSeedInternal(char[] normalisedMnemonic, String passphrase) { + try { + // Normalise passphrase and convert to byte array + passphrase=Normalizer.normalize(passphrase, Normalizer.Form.NFKD); + byte[] salt = ("mnemonic"+passphrase).getBytes(StandardCharsets.UTF_8); + + // Generate seed + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); + KeySpec keyspec = new PBEKeySpec(normalisedMnemonic, salt, 2048, SEED_LENGTH * 8); + Key key = factory.generateSecret(keyspec); + + // Wrap result as Blob + byte[] bs = key.getEncoded(); + return Blob.wrap(bs); + } catch (NoSuchAlgorithmException| InvalidKeySpecException e) { + throw new Error("Security error getting BIP39 seed",e); + } } - - public static String createSecureMnemonic() { return createSecureMnemonic(12); } diff --git a/convex-core/src/main/java/convex/core/crypto/SLIP10.java b/convex-core/src/main/java/convex/core/crypto/SLIP10.java index 759530c01..ebbacaf60 100644 --- a/convex-core/src/main/java/convex/core/crypto/SLIP10.java +++ b/convex-core/src/main/java/convex/core/crypto/SLIP10.java @@ -4,7 +4,6 @@ import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.util.Arrays; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; @@ -53,7 +52,8 @@ public static Blob getMaster(Blob seed) { * @param master Master key as defined in SLIP10 * @param ixs key derivation path indexes */ - public static Blob deriveKey(Blob master, int... ixs) { + public static Blob derive(Blob master, int... ixs) { + if (ixs.length==0) return master; try { byte[] bs=master.getBytes(); if (bs.length!=64) throw new Error("Invalid SLIP10 master key, must be 64 bytes"); @@ -72,8 +72,7 @@ public static Blob deriveKey(Blob master, int... ixs) { } // Wrap the bytes of the newly derived seed to get the derived Ed25519 key - Blob result= Blob.create(bs,0,AKeyPair.SEED_LENGTH); - Arrays.fill(bs, (byte) 0); + Blob result= Blob.wrap(bs); return result; } catch (Exception e) { throw new Error("Failure in SLIP-10!!!",e); @@ -82,7 +81,7 @@ public static Blob deriveKey(Blob master, int... ixs) { public static AKeyPair deriveKeyPair(Blob seed, int... ixs) { Blob master = getMaster(seed); - Blob keySeed = deriveKey(master,ixs); + Blob keySeed = derive(master,ixs).slice(0, 32); AKeyPair kp=AKeyPair.create(keySeed); return kp; } diff --git a/convex-core/src/test/java/convex/core/crypto/SLIP10Test.java b/convex-core/src/test/java/convex/core/crypto/SLIP10Test.java index 4ce8f228f..db67c2526 100644 --- a/convex-core/src/test/java/convex/core/crypto/SLIP10Test.java +++ b/convex-core/src/test/java/convex/core/crypto/SLIP10Test.java @@ -24,22 +24,22 @@ public void testSLIP10Vector1() throws InvalidKeyException, NoSuchAlgorithmExcep assertEquals("2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7",ms.toHexString()); assertEquals("a4b2856bfec510abab89753fac1ac0e1112364e7d250545963f135f2a33188ed",AKeyPair.create(ms).getAccountKey().toHexString()); - assertEquals(m.slice(0,32),SLIP10.deriveKey(m)); + assertEquals(m,SLIP10.derive(m)); - Blob m_0h=SLIP10.deriveKey(m, 0); - assertEquals("68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3",m_0h.toHexString()); + Blob m_0h=SLIP10.derive(m, 0); + assertEquals("68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3",m_0h.slice(0,32).toHexString()); - Blob m_0h_1h=SLIP10.deriveKey(m, 0,1); - assertEquals("b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2",m_0h_1h.toHexString()); + Blob m_0h_1h=SLIP10.derive(m, 0,1); + assertEquals("b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2",m_0h_1h.slice(0,32).toHexString()); - Blob m_0h_1h_2h=SLIP10.deriveKey(m, 0,1,2); - assertEquals("92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9",m_0h_1h_2h.toHexString()); + Blob m_0h_1h_2h=SLIP10.derive(m, 0,1,2); + assertEquals("92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9",m_0h_1h_2h.slice(0,32).toHexString()); - Blob m_0h_1h_2h_2h=SLIP10.deriveKey(m, 0,1,2,2); - assertEquals("30d1dc7e5fc04c31219ab25a27ae00b50f6fd66622f6e9c913253d6511d1e662",m_0h_1h_2h_2h.toHexString()); + Blob m_0h_1h_2h_2h=SLIP10.derive(m, 0,1,2,2); + assertEquals("30d1dc7e5fc04c31219ab25a27ae00b50f6fd66622f6e9c913253d6511d1e662",m_0h_1h_2h_2h.slice(0,32).toHexString()); - Blob m_last=SLIP10.deriveKey(m, 0,1,2,2,1000000000); - assertEquals("8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793",m_last.toHexString()); + Blob m_last=SLIP10.derive(m, 0,1,2,2,1000000000); + assertEquals("8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793",m_last.slice(0,32).toHexString()); } @Test @@ -52,22 +52,34 @@ public void testSLIP10Vector2() throws InvalidKeyException, NoSuchAlgorithmExcep assertEquals("171cb88b1b3c1db25add599712e36245d75bc65a1a5c9e18d76f9f2b1eab4012",ms.toHexString()); assertEquals("8fe9693f8fa62a4305a140b9764c5ee01e455963744fe18204b4fb948249308a",AKeyPair.create(ms).getAccountKey().toHexString()); - assertEquals(m.slice(0,32),SLIP10.deriveKey(m)); + assertEquals(m,SLIP10.derive(m)); - Blob m_0h=SLIP10.deriveKey(m, 0); - assertEquals("1559eb2bbec5790b0c65d8693e4d0875b1747f4970ae8b650486ed7470845635",m_0h.toHexString()); + Blob m_0h=SLIP10.derive(m, 0); + assertEquals("1559eb2bbec5790b0c65d8693e4d0875b1747f4970ae8b650486ed7470845635",m_0h.slice(0,32).toHexString()); - Blob m_0h_1h=SLIP10.deriveKey(m, 0,2147483647); - assertEquals("ea4f5bfe8694d8bb74b7b59404632fd5968b774ed545e810de9c32a4fb4192f4",m_0h_1h.toHexString()); + Blob m_0h_1h=SLIP10.derive(m, 0,2147483647); + assertEquals("ea4f5bfe8694d8bb74b7b59404632fd5968b774ed545e810de9c32a4fb4192f4",m_0h_1h.slice(0,32).toHexString()); - Blob m_0h_1h_2h=SLIP10.deriveKey(m, 0,2147483647,1); - assertEquals("3757c7577170179c7868353ada796c839135b3d30554bbb74a4b1e4a5a58505c",m_0h_1h_2h.toHexString()); + Blob m_0h_1h_2h=SLIP10.derive(m, 0,2147483647,1); + assertEquals("3757c7577170179c7868353ada796c839135b3d30554bbb74a4b1e4a5a58505c",m_0h_1h_2h.slice(0,32).toHexString()); - Blob m_0h_1h_2h_2h=SLIP10.deriveKey(m, 0,2147483647,1,2147483646); - assertEquals("5837736c89570de861ebc173b1086da4f505d4adb387c6a1b1342d5e4ac9ec72",m_0h_1h_2h_2h.toHexString()); + Blob m_0h_1h_2h_2h=SLIP10.derive(m, 0,2147483647,1,2147483646); + assertEquals("5837736c89570de861ebc173b1086da4f505d4adb387c6a1b1342d5e4ac9ec72",m_0h_1h_2h_2h.slice(0,32).toHexString()); - Blob m_last=SLIP10.deriveKey(m, 0,2147483647,1,2147483646,2); - assertEquals("551d333177df541ad876a60ea71f00447931c0a9da16f227c11ea080d7391b8d",m_last.toHexString()); + Blob m_last=SLIP10.derive(m, 0,2147483647,1,2147483646,2); + assertEquals("551d333177df541ad876a60ea71f00447931c0a9da16f227c11ea080d7391b8d",m_last.slice(0,32).toHexString()); + } + + @Test + public void testCAD25 () { + String seedPhrase="hold round save brand meat deposit armed idea taste reunion silent pair estate ladder copper"; + Blob seed=BIP39.getSeed(seedPhrase, "test"); + // Verified using tool: https://iancoleman.io/bip39/ + assertEquals("d46c4e60d0137e7ee0acc8b836d76d9a0458705caa128899709f576bade690b3c7cba49ece50a211193b9eb7803be49d02c8ddae02c3b88790ac17fa72f219a6",seed.toHexString()); + + AKeyPair kp=SLIP10.deriveKeyPair(seed, 44, 888, 1234,0,1); + Blob priv=kp.getSeed(); + assertEquals("2172bb864deb4f978ad6360beefe205d38a6839c011dc4f37592769007c8321f",priv.toHexString()); } @Test @@ -87,12 +99,12 @@ public void testSLIP10EdgeCases () throws InvalidKeyException, NoSuchAlgorithmEx Blob m=SLIP10.getMaster(seed); // hardened and normal indexes produce the same result for Ed25519 - assertEquals(SLIP10.deriveKey(m, 0),SLIP10.deriveKey(m, 0x80000000)); + assertEquals(SLIP10.derive(m, 0),SLIP10.derive(m, 0x80000000)); } @Test public void testSLIP10Fails () { - assertThrows(Error.class,()->SLIP10.deriveKey(Blobs.empty(),1,2)); + assertThrows(Error.class,()->SLIP10.derive(Blobs.empty(),1,2)); } }