Skip to content

Commit

Permalink
Modify SLIP-10 derive API to return full derived key
Browse files Browse the repository at this point in the history
  • Loading branch information
mikera committed Mar 17, 2024
1 parent 67a17e8 commit 101cc60
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 72 deletions.
39 changes: 17 additions & 22 deletions convex-cli/src/main/java/convex/cli/key/KeyGenerate.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package convex.cli.key;

import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.Arrays;

Expand Down Expand Up @@ -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();
}
}

Expand Down
44 changes: 23 additions & 21 deletions convex-core/src/main/java/convex/core/crypto/BIP39.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand All @@ -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);
}
Expand Down
9 changes: 4 additions & 5 deletions convex-core/src/main/java/convex/core/crypto/SLIP10.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand All @@ -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);
Expand All @@ -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;
}
Expand Down
60 changes: 36 additions & 24 deletions convex-core/src/test/java/convex/core/crypto/SLIP10Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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));
}

}

0 comments on commit 101cc60

Please sign in to comment.