From 9e5a7d0e8d31eb4e21d543081c23f7f0aaef9795 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 15 May 2020 17:56:14 +0200 Subject: [PATCH] electrum seed version system support --- .../java/com/sparrowwallet/drongo/Utils.java | 22 ++++- .../drongo/wallet/DeterministicSeed.java | 93 ++++++++++++++----- .../drongo/wallet/ElectrumMnemonicCode.java | 62 +++++++++++++ .../sparrowwallet/drongo/wallet/Keystore.java | 8 -- 4 files changed, 150 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/drongo/wallet/ElectrumMnemonicCode.java diff --git a/src/main/java/com/sparrowwallet/drongo/Utils.java b/src/main/java/com/sparrowwallet/drongo/Utils.java index 3d3cbc5..0ff1b47 100644 --- a/src/main/java/com/sparrowwallet/drongo/Utils.java +++ b/src/main/java/com/sparrowwallet/drongo/Utils.java @@ -1,6 +1,9 @@ package com.sparrowwallet.drongo; +import com.sparrowwallet.drongo.crypto.AESKeyCrypter; import com.sparrowwallet.drongo.crypto.ChildNumber; +import com.sparrowwallet.drongo.crypto.EncryptedData; +import com.sparrowwallet.drongo.crypto.KeyCrypter; import com.sparrowwallet.drongo.protocol.ProtocolException; import com.sparrowwallet.drongo.protocol.Ripemd160; import com.sparrowwallet.drongo.protocol.Sha256Hash; @@ -13,10 +16,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.StringJoiner; +import java.util.*; public class Utils { public static final int MAX_INITIAL_ARRAY_LENGTH = 20; @@ -248,6 +248,20 @@ public class Utils { return Ripemd160.getHash(sha256); } + /** + * Calculates RIPEMD160(SHA256(input)). This is used in Address calculations. + */ + public static byte[] sha256sha256(byte[] input) { + byte[] sha256 = Sha256Hash.hash(input); + return Sha256Hash.hash(sha256); + } + + public static byte[] decryptAesCbcPkcs7(byte[] initializationVector, byte[] encryptedBytes, byte[] keyBytes) { + KeyCrypter keyCrypter = new AESKeyCrypter(); + EncryptedData data = new EncryptedData(initializationVector, encryptedBytes); + return keyCrypter.decrypt(data, new KeyParameter(keyBytes)); + } + /** Convert to a string path, starting with "M/" */ public static String formatHDPath(List path) { StringJoiner joiner = new StringJoiner("/"); diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java b/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java index 1817a64..8f3f834 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java @@ -15,6 +15,8 @@ public class DeterministicSeed implements EncryptableItem { public static final int DEFAULT_SEED_ENTROPY_BITS = 128; public static final int MAX_SEED_ENTROPY_BITS = 512; + private final Type type; + private final byte[] seed; private final List mnemonicCode; @@ -23,40 +25,45 @@ public class DeterministicSeed implements EncryptableItem { private long creationTimeSeconds; - public DeterministicSeed(String mnemonicString, byte[] seed, String passphrase, long creationTimeSeconds) { - this(decodeMnemonicCode(mnemonicString), seed, passphrase, creationTimeSeconds); + //Session only storage + private transient String passphrase; + + public DeterministicSeed(String mnemonicString, byte[] seed, String passphrase, long creationTimeSeconds, Type type) { + this(decodeMnemonicCode(mnemonicString), seed, passphrase, creationTimeSeconds, type); } - public DeterministicSeed(byte[] seed, List mnemonic, long creationTimeSeconds) { + public DeterministicSeed(byte[] seed, List mnemonic, long creationTimeSeconds, Type type) { this.seed = seed; this.encryptedSeed = null; this.mnemonicCode = mnemonic; this.encryptedMnemonicCode = null; this.creationTimeSeconds = creationTimeSeconds; + this.type = type; } - public DeterministicSeed(EncryptedData encryptedMnemonic, EncryptedData encryptedSeed, long creationTimeSeconds) { + public DeterministicSeed(EncryptedData encryptedMnemonic, EncryptedData encryptedSeed, long creationTimeSeconds, Type type) { this.seed = null; this.encryptedSeed = encryptedSeed; this.mnemonicCode = null; this.encryptedMnemonicCode = encryptedMnemonic; this.creationTimeSeconds = creationTimeSeconds; + this.type = type; } /** - * Constructs a seed from a BIP 39 mnemonic code. See {@link Bip39MnemonicCode} for more + * Constructs a seed from a mnemonic code. See {@link Bip39MnemonicCode} or {@link ElectrumMnemonicCode} for more * details on this scheme. * @param mnemonicCode A list of words. * @param seed The derived seed, or pass null to derive it from mnemonicCode (slow) * @param passphrase A user supplied passphrase, or an empty string if there is no passphrase * @param creationTimeSeconds When the seed was originally created, UNIX time. */ - public DeterministicSeed(List mnemonicCode, byte[] seed, String passphrase, long creationTimeSeconds) { - this((seed != null ? seed : Bip39MnemonicCode.toSeed(mnemonicCode, passphrase)), mnemonicCode, creationTimeSeconds); + public DeterministicSeed(List mnemonicCode, byte[] seed, String passphrase, long creationTimeSeconds, Type type) { + this((seed != null ? seed : type.toSeed(mnemonicCode, passphrase)), mnemonicCode, creationTimeSeconds, type); } /** - * Constructs a seed from a BIP 39 mnemonic code. See {@link Bip39MnemonicCode} for more + * Constructs a new BIP39 seed. See {@link Bip39MnemonicCode} for more * details on this scheme. * @param random Entropy source * @param bits number of bits, must be divisible by 32 @@ -67,7 +74,7 @@ public class DeterministicSeed implements EncryptableItem { } /** - * Constructs a seed from a BIP 39 mnemonic code. See {@link Bip39MnemonicCode} for more + * Constructs a BIP39 seed from provided entropy. See {@link Bip39MnemonicCode} for more * details on this scheme. * @param entropy entropy bits, length must be divisible by 32 * @param passphrase A user supplied passphrase, or an empty string if there is no passphrase @@ -96,14 +103,15 @@ public class DeterministicSeed implements EncryptableItem { this.encryptedMnemonicCode = null; this.encryptedSeed = null; this.creationTimeSeconds = creationTimeSeconds; + this.type = Type.BIP39; } - public boolean needPassphrase() { + public boolean usesPassphrase() { if(isEncrypted()) { throw new IllegalArgumentException("Cannot determine if passphrase is required in encrypted state"); } - byte[] mnemonicOnlySeed = Bip39MnemonicCode.toSeed(mnemonicCode, ""); + byte[] mnemonicOnlySeed = type.toSeed(mnemonicCode, ""); return Arrays.equals(mnemonicOnlySeed, seed); } @@ -172,6 +180,10 @@ public class DeterministicSeed implements EncryptableItem { this.creationTimeSeconds = creationTimeSeconds; } + public Type getType() { + return type; + } + public DeterministicSeed encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) { if(encryptedSeed != null) { throw new IllegalArgumentException("Trying to encrypt seed twice"); @@ -181,7 +193,7 @@ public class DeterministicSeed implements EncryptableItem { } EncryptedData encryptedMnemonic = keyCrypter.encrypt(getMnemonicAsBytes(), null, aesKey); EncryptedData encryptedSeed = keyCrypter.encrypt(seed, null, aesKey); - return new DeterministicSeed(encryptedMnemonic, encryptedSeed, creationTimeSeconds); + return new DeterministicSeed(encryptedMnemonic, encryptedSeed, creationTimeSeconds, type); } private byte[] getMnemonicAsBytes() { @@ -194,15 +206,15 @@ public class DeterministicSeed implements EncryptableItem { } List mnemonic = decodeMnemonicCode(crypter.decrypt(encryptedMnemonicCode, aesKey)); byte[] seed = encryptedSeed == null ? null : crypter.decrypt(encryptedSeed, aesKey); - return new DeterministicSeed(mnemonic, seed, passphrase, creationTimeSeconds); + return new DeterministicSeed(mnemonic, seed, passphrase, creationTimeSeconds, type); } - public DeterministicSeed setPassphrase(String passphrase) { - if(isEncrypted()) { - throw new UnsupportedOperationException("Cannot set passphrase on encrypted seed"); - } + public String getPassphrase() { + return passphrase; + } - return new DeterministicSeed(mnemonicCode, seed, passphrase, creationTimeSeconds); + public void setPassphrase(String passphrase) { + this.passphrase = passphrase; } @Override @@ -227,13 +239,13 @@ public class DeterministicSeed implements EncryptableItem { * @throws MnemonicException if check fails */ public void check() throws MnemonicException { - if (mnemonicCode != null) { - Bip39MnemonicCode.INSTANCE.check(mnemonicCode); + if(mnemonicCode != null) { + type.check(mnemonicCode); } } byte[] getEntropyBytes() throws MnemonicException { - return Bip39MnemonicCode.INSTANCE.toEntropy(mnemonicCode); + return type.getEntropyBytes(mnemonicCode); } /** Get the mnemonic code, or null if unknown. */ @@ -265,9 +277,44 @@ public class DeterministicSeed implements EncryptableItem { public DeterministicSeed copy() { if(isEncrypted()) { - return new DeterministicSeed(encryptedMnemonicCode.copy(), encryptedSeed.copy(), creationTimeSeconds); + return new DeterministicSeed(encryptedMnemonicCode.copy(), encryptedSeed.copy(), creationTimeSeconds, type); } - return new DeterministicSeed(Arrays.copyOf(seed, seed.length), new ArrayList<>(mnemonicCode), creationTimeSeconds); + return new DeterministicSeed(Arrays.copyOf(seed, seed.length), new ArrayList<>(mnemonicCode), creationTimeSeconds, type); + } + + public enum Type { + BIP39() { + public byte[] getEntropyBytes(List mnemonicCode) throws MnemonicException { + return Bip39MnemonicCode.INSTANCE.toEntropy(mnemonicCode); + } + + public void check(List mnemonicCode) throws MnemonicException { + Bip39MnemonicCode.INSTANCE.check(mnemonicCode); + } + + public byte[] toSeed(List mnemonicCode, String passphrase) { + return Bip39MnemonicCode.toSeed(mnemonicCode, passphrase); + } + }, + ELECTRUM() { + public byte[] getEntropyBytes(List mnemonicCode) throws MnemonicException { + throw new MnemonicException("Electrum seeds do not provide entropy bytes"); + } + + public void check(List mnemonicCode) throws MnemonicException { + ElectrumMnemonicCode.INSTANCE.check(mnemonicCode); + } + + public byte[] toSeed(List mnemonicCode, String passphrase) { + return ElectrumMnemonicCode.toSeed(mnemonicCode, passphrase); + } + }; + + public abstract byte[] getEntropyBytes(List mnemonicCode) throws MnemonicException; + + public abstract void check(List mnemonicCode) throws MnemonicException; + + public abstract byte[] toSeed(List mnemonicCode, String passphrase); } } diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/ElectrumMnemonicCode.java b/src/main/java/com/sparrowwallet/drongo/wallet/ElectrumMnemonicCode.java new file mode 100644 index 0000000..fae0782 --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/wallet/ElectrumMnemonicCode.java @@ -0,0 +1,62 @@ +package com.sparrowwallet.drongo.wallet; + +import com.sparrowwallet.drongo.Utils; + +import java.nio.charset.StandardCharsets; +import java.text.Normalizer; +import java.util.List; + +public class ElectrumMnemonicCode { + private static final int PBKDF2_ROUNDS = 2048; + + private final List VALID_PREFIXES = List.of("01", "100", "101"); + + public static ElectrumMnemonicCode INSTANCE = new ElectrumMnemonicCode(); + + /** + * Gets the word list this code uses. + */ + public List getWordList() { + return Bip39MnemonicCode.INSTANCE.getWordList(); + } + + public static byte[] toSeed(List seedWords, String passphrase) { + String mnemonicWords = String.join(" ", seedWords); + return toSeed(mnemonicWords, passphrase); + } + + public static byte[] toSeed(String mnemonicWords, String passphrase) { + if(passphrase == null) { + passphrase = ""; + } + + String mnemonic = Normalizer.normalize(mnemonicWords, Normalizer.Form.NFKD); + String salt = "electrum" + Normalizer.normalize(passphrase, Normalizer.Form.NFKD); + + return Utils.getPbkdf2HmacSha512Hash(mnemonic.getBytes(StandardCharsets.UTF_8), salt.getBytes(StandardCharsets.UTF_8), PBKDF2_ROUNDS); + } + + /** + * Check to see if a mnemonic word list is valid. + */ + public void check(List words) throws MnemonicException { + String prefix = getPrefix(words); + if(!VALID_PREFIXES.contains(prefix)) { + throw new MnemonicException("Invalid prefix " + prefix); + } + } + + private String getPrefix(List words) throws MnemonicException { + String mnemonic = String.join(" ", words); + mnemonic = Normalizer.normalize(mnemonic, Normalizer.Form.NFKD); + byte [] hash = Utils.getHmacSha512Hash("Seed version".getBytes(StandardCharsets.UTF_8), mnemonic.getBytes(StandardCharsets.UTF_8)); + String hex = Utils.bytesToHex(hash); + try { + int prefixLength = Integer.parseInt(hex.substring(0, 1)) + 2; + String prefix = hex.substring(0, prefixLength); + return Integer.toHexString(Integer.parseInt(prefix, 16)); + } catch(NumberFormatException e) { + throw new MnemonicException("Invalid prefix bytes"); + } + } +} diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java b/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java index 9a844c3..bc4adcb 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java @@ -172,14 +172,6 @@ public class Keystore { return keystore; } - public void setPassphrase(String passphrase) { - if(seed != null) { - seed = seed.setPassphrase(passphrase); - } else { - throw new UnsupportedOperationException("Cannot set passphrase on a keystore without a seed"); - } - } - public boolean hasSeed() { return seed != null; }