mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-27 02:26:44 +00:00
add bip39 seed calculator
This commit is contained in:
parent
019a3cf34f
commit
27dda91576
6 changed files with 2251 additions and 8 deletions
|
@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.protocol.ProtocolException;
|
||||||
import com.sparrowwallet.drongo.protocol.Ripemd160;
|
import com.sparrowwallet.drongo.protocol.Ripemd160;
|
||||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
import org.bouncycastle.crypto.digests.SHA512Digest;
|
import org.bouncycastle.crypto.digests.SHA512Digest;
|
||||||
|
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
|
||||||
import org.bouncycastle.crypto.macs.HMac;
|
import org.bouncycastle.crypto.macs.HMac;
|
||||||
import org.bouncycastle.crypto.params.KeyParameter;
|
import org.bouncycastle.crypto.params.KeyParameter;
|
||||||
|
|
||||||
|
@ -264,14 +265,18 @@ public class Utils {
|
||||||
return Collections.unmodifiableList(childPath);
|
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();
|
SHA512Digest digest = new SHA512Digest();
|
||||||
HMac hMac = new HMac(digest);
|
HMac hMac = new HMac(digest);
|
||||||
hMac.init(new KeyParameter(key));
|
hMac.init(new KeyParameter(key));
|
||||||
return hMac;
|
return hMac;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] hmacSha512(HMac hmacSha512, byte[] input) {
|
private static byte[] getHmacSha512Hash(HMac hmacSha512, byte[] input) {
|
||||||
hmacSha512.reset();
|
hmacSha512.reset();
|
||||||
hmacSha512.update(input, 0, input.length);
|
hmacSha512.update(input, 0, input.length);
|
||||||
byte[] out = new byte[64];
|
byte[] out = new byte[64];
|
||||||
|
@ -279,7 +284,9 @@ public class Utils {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] hmacSha512(byte[] key, byte[] data) {
|
public static byte[] getPbkdf2HmacSha512Hash(byte[] preimage, byte[] salt, int iterationCount) {
|
||||||
return hmacSha512(createHmacSha512Digest(key), data);
|
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA512Digest());
|
||||||
|
gen.init(preimage, salt, iterationCount);
|
||||||
|
return ((KeyParameter) gen.generateDerivedParameters(512)).getKey();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -621,9 +621,7 @@ public class ECKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ECKey createKeyPbkdf2HmacSha512(String password, byte[] salt, int iterationCount) {
|
public static ECKey createKeyPbkdf2HmacSha512(String password, byte[] salt, int iterationCount) {
|
||||||
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA512Digest());
|
byte[] secret = Utils.getPbkdf2HmacSha512Hash(password.getBytes(StandardCharsets.UTF_8), salt, iterationCount);
|
||||||
gen.init(password.getBytes(StandardCharsets.UTF_8), salt, iterationCount);
|
|
||||||
byte[] secret = ((KeyParameter) gen.generateDerivedParameters(512)).getKey();
|
|
||||||
return ECKey.fromPrivate(secret);
|
return ECKey.fromPrivate(secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ public class HDKeyDerivation {
|
||||||
ByteBuffer data = ByteBuffer.allocate(37);
|
ByteBuffer data = ByteBuffer.allocate(37);
|
||||||
data.put(parentPublicKey);
|
data.put(parentPublicKey);
|
||||||
data.putInt(childNumber.i());
|
data.putInt(childNumber.i());
|
||||||
byte[] i = Utils.hmacSha512(parent.getChainCode(), data.array());
|
byte[] i = Utils.getHmacSha512Hash(parent.getChainCode(), data.array());
|
||||||
if(i.length != 64) {
|
if(i.length != 64) {
|
||||||
throw new IllegalStateException("HmacSHA512 output must be 64 bytes, is" + i.length);
|
throw new IllegalStateException("HmacSHA512 output must be 64 bytes, is" + i.length);
|
||||||
}
|
}
|
||||||
|
|
103
src/main/java/com/sparrowwallet/drongo/wallet/Bip39.java
Normal file
103
src/main/java/com/sparrowwallet/drongo/wallet/Bip39.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2048
src/main/resources/wordlist/bip39-english.txt
Normal file
2048
src/main/resources/wordlist/bip39-english.txt
Normal file
File diff suppressed because it is too large
Load diff
87
src/test/java/com/sparrowwallet/drongo/wallet/Bip39Test.java
Normal file
87
src/test/java/com/sparrowwallet/drongo/wallet/Bip39Test.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue