add bip39 seed calculator

This commit is contained in:
Craig Raw 2020-05-05 09:26:29 +02:00
parent 019a3cf34f
commit 27dda91576
6 changed files with 2251 additions and 8 deletions

View file

@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.protocol.ProtocolException;
import com.sparrowwallet.drongo.protocol.Ripemd160;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
@ -264,14 +265,18 @@ public class Utils {
return Collections.unmodifiableList(childPath);
}
static HMac createHmacSha512Digest(byte[] key) {
public static byte[] getHmacSha512Hash(byte[] key, byte[] data) {
return getHmacSha512Hash(createHmacSha512Digest(key), data);
}
private static HMac createHmacSha512Digest(byte[] key) {
SHA512Digest digest = new SHA512Digest();
HMac hMac = new HMac(digest);
hMac.init(new KeyParameter(key));
return hMac;
}
public static byte[] hmacSha512(HMac hmacSha512, byte[] input) {
private static byte[] getHmacSha512Hash(HMac hmacSha512, byte[] input) {
hmacSha512.reset();
hmacSha512.update(input, 0, input.length);
byte[] out = new byte[64];
@ -279,7 +284,9 @@ public class Utils {
return out;
}
public static byte[] hmacSha512(byte[] key, byte[] data) {
return hmacSha512(createHmacSha512Digest(key), data);
public static byte[] getPbkdf2HmacSha512Hash(byte[] preimage, byte[] salt, int iterationCount) {
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA512Digest());
gen.init(preimage, salt, iterationCount);
return ((KeyParameter) gen.generateDerivedParameters(512)).getKey();
}
}

View file

@ -621,9 +621,7 @@ public class ECKey {
}
public static ECKey createKeyPbkdf2HmacSha512(String password, byte[] salt, int iterationCount) {
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA512Digest());
gen.init(password.getBytes(StandardCharsets.UTF_8), salt, iterationCount);
byte[] secret = ((KeyParameter) gen.generateDerivedParameters(512)).getKey();
byte[] secret = Utils.getPbkdf2HmacSha512Hash(password.getBytes(StandardCharsets.UTF_8), salt, iterationCount);
return ECKey.fromPrivate(secret);
}

View file

@ -26,7 +26,7 @@ public class HDKeyDerivation {
ByteBuffer data = ByteBuffer.allocate(37);
data.put(parentPublicKey);
data.putInt(childNumber.i());
byte[] i = Utils.hmacSha512(parent.getChainCode(), data.array());
byte[] i = Utils.getHmacSha512Hash(parent.getChainCode(), data.array());
if(i.length != 64) {
throw new IllegalStateException("HmacSHA512 output must be 64 bytes, is" + i.length);
}

View file

@ -0,0 +1,103 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.text.Normalizer;
import java.util.*;
public class Bip39 {
private Map<String, Integer> wordlistIndex;
public byte[] getSeed(List<String> mnemonicWords, String passphrase) {
loadWordlistIndex();
int concatLength = mnemonicWords.size() * 11;
StringBuilder concat = new StringBuilder();
for(String mnemonicWord : mnemonicWords) {
Integer index = wordlistIndex.get(mnemonicWord);
if (index == null) {
throw new IllegalArgumentException("Provided mnemonic word \"" + mnemonicWord + "\" is not in the BIP39 english word list");
}
String binaryIndex = addLeadingZeros(Integer.toBinaryString(index), 11);
concat.append(binaryIndex, 0, 11);
}
int checksumLength = concatLength / 33;
int entropyLength = concatLength - checksumLength;
byte[] entropy = byteArrayFromBinaryString(concat.substring(0, entropyLength));
String providedChecksum = concat.substring(entropyLength);
byte[] sha256 = Sha256Hash.hash(entropy);
String calculatedChecksum = addLeadingZeros(Integer.toBinaryString(Byte.toUnsignedInt(sha256[0])), 8).substring(0, checksumLength);
if(!providedChecksum.equals(calculatedChecksum)) {
throw new IllegalArgumentException("Provided mnemonic words do not represent a valid BIP39 seed: checksum failed");
}
String saltStr = "mnemonic";
if(passphrase != null) {
saltStr += Normalizer.normalize(passphrase, Normalizer.Form.NFKD);
}
byte[] salt = saltStr.getBytes(StandardCharsets.UTF_8);
String mnemonic = String.join(" ", mnemonicWords);
mnemonic = Normalizer.normalize(mnemonic, Normalizer.Form.NFKD);
return Utils.getPbkdf2HmacSha512Hash(mnemonic.getBytes(StandardCharsets.UTF_8), salt, 2048);
}
public String addLeadingZeros(String s, int length) {
if (s.length() >= length) return s;
else return String.format("%0" + (length-s.length()) + "d%s", 0, s);
}
private byte[] byteArrayFromBinaryString(String binaryString) {
int splitSize = 8;
if(binaryString.length() < splitSize) {
binaryString = addLeadingZeros(binaryString, 8);
}
if(binaryString.length() % splitSize == 0){
int index = 0;
int position = 0;
byte[] resultByteArray = new byte[binaryString.length()/splitSize];
StringBuilder text = new StringBuilder(binaryString);
while (index < text.length()) {
String binaryStringChunk = text.substring(index, Math.min(index + splitSize, text.length()));
int byteAsInt = Integer.parseInt(binaryStringChunk, 2);
resultByteArray[position] = (byte)byteAsInt;
index += splitSize;
position ++;
}
return resultByteArray;
}
else {
throw new IllegalArgumentException("Cannot convert binary string to byte[], because of the input length '" + binaryString + "' % 8 != 0");
}
}
private void loadWordlistIndex() {
if(wordlistIndex == null) {
wordlistIndex = new HashMap<>();
try{
BufferedReader reader = new BufferedReader(new InputStreamReader(this.getClass().getResourceAsStream("/wordlist/bip39-english.txt"), StandardCharsets.UTF_8));
String line;
for(int i = 0; (line = reader.readLine()) != null; i++) {
wordlistIndex.put(line.trim(), i);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,87 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.Utils;
import org.junit.Assert;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
public class Bip39Test {
@Test
public void bip39TwelveWordsTest() {
String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor";
List<String> wordlist = Arrays.asList(words.split(" "));
Bip39 bip39 = new Bip39();
byte[] seed = bip39.getSeed(wordlist, "");
Assert.assertEquals("727ecfcf0bce9d8ec0ef066f7aeb845c271bdd4ee06a37398cebd40dc810140bb620b6c10a8ad671afdceaf37aa55d92d6478f747e8b92430dd938ab5be961dd", Utils.bytesToHex(seed));
}
@Test
public void bip39TwelveWordsPassphraseTest() {
String words = "arch easily near social civil image seminar monkey engine party promote turtle";
List<String> wordlist = Arrays.asList(words.split(" "));
Bip39 bip39 = new Bip39();
byte[] seed = bip39.getSeed(wordlist, "anotherpass867");
Assert.assertEquals("ca50764cda44a2cf52aef3c677bebf26011f9dc2b9fddfed2a8a5a9ecb8542956990a16e6873b7724044e83708d9d3a662b765e8800e6e79b289f51c2bcad756", Utils.bytesToHex(seed));
}
@Test
public void bip39FifteenWordsTest() {
String words = "open grunt omit snap behave inch engine hamster hope increase exotic segment news choose roast";
List<String> wordlist = Arrays.asList(words.split(" "));
Bip39 bip39 = new Bip39();
byte[] seed = bip39.getSeed(wordlist, "");
Assert.assertEquals("2174deae5fd315253dc065db7ef97f46957eb68a12505adccfb7f8aca5b63788c587e73430848f85417d9a7d95e6396d2eb3af73c9fb507ebcb9268a5ad47885", Utils.bytesToHex(seed));
}
@Test
public void bip39EighteenWordsTest() {
String words = "mandate lend daring actual health dilemma throw muffin garden pony inherit volume slim visual police supreme bless crush";
List<String> wordlist = Arrays.asList(words.split(" "));
Bip39 bip39 = new Bip39();
byte[] seed = bip39.getSeed(wordlist, "");
Assert.assertEquals("04bd65f582e288bbf595213048b06e1552017776d20ca290ac06d840e197bcaaccd4a85a45a41219be4183dd2e521e7a7a2d6aea3069f04e503ef6d9c8dfa651", Utils.bytesToHex(seed));
}
@Test
public void bip39TwentyOneWordsTest() {
String words = "mirror milk file hope drill conduct empty mutual physical easily sell patient green final release excuse name asset update advance resource";
List<String> wordlist = Arrays.asList(words.split(" "));
Bip39 bip39 = new Bip39();
byte[] seed = bip39.getSeed(wordlist, "");
Assert.assertEquals("f3a88a437153333f9759f323dfe7910e6a649c34da5800e6c978d77baad54b67b06eab17c0107243f3e8b395a2de98c910e9528127539efda2eea5ae50e94019", Utils.bytesToHex(seed));
}
@Test
public void bip39TwentyFourWordsTest() {
String words = "earth easily dwarf dance forum muscle brick often huge base long steel silk frost quiz liquid echo adapt annual expand slim rookie venture oval";
List<String> wordlist = Arrays.asList(words.split(" "));
Bip39 bip39 = new Bip39();
byte[] seed = bip39.getSeed(wordlist, "");
Assert.assertEquals("60f825219a1fcfa479de28435e9bf2aa5734e212982daee582ca0427ad6141c65be9863c3ce0f18e2b173083ea49dcf47d07148734a5f748ac60d470cee6a2bc", Utils.bytesToHex(seed));
}
@Test
public void bip39TwentyFourWordsPassphraseTest() {
String words = "earth easily dwarf dance forum muscle brick often huge base long steel silk frost quiz liquid echo adapt annual expand slim rookie venture oval";
List<String> wordlist = Arrays.asList(words.split(" "));
Bip39 bip39 = new Bip39();
byte[] seed = bip39.getSeed(wordlist, "thispass");
Assert.assertEquals("a652d123f421f56257391af26063e900619678b552dafd3850e699f6da0667269bbcaebb0509557481db29607caac0294b3cd337d740174cfa05f552fe9e0272", Utils.bytesToHex(seed));
}
}