electrum seed version system support

This commit is contained in:
Craig Raw 2020-05-15 17:56:14 +02:00
parent 6b1f7d0174
commit 9e5a7d0e8d
4 changed files with 150 additions and 35 deletions

View file

@ -1,6 +1,9 @@
package com.sparrowwallet.drongo; package com.sparrowwallet.drongo;
import com.sparrowwallet.drongo.crypto.AESKeyCrypter;
import com.sparrowwallet.drongo.crypto.ChildNumber; 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.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;
@ -13,10 +16,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.List;
import java.util.StringJoiner;
public class Utils { public class Utils {
public static final int MAX_INITIAL_ARRAY_LENGTH = 20; public static final int MAX_INITIAL_ARRAY_LENGTH = 20;
@ -248,6 +248,20 @@ public class Utils {
return Ripemd160.getHash(sha256); 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/" */ /** Convert to a string path, starting with "M/" */
public static String formatHDPath(List<ChildNumber> path) { public static String formatHDPath(List<ChildNumber> path) {
StringJoiner joiner = new StringJoiner("/"); StringJoiner joiner = new StringJoiner("/");

View file

@ -15,6 +15,8 @@ public class DeterministicSeed implements EncryptableItem {
public static final int DEFAULT_SEED_ENTROPY_BITS = 128; public static final int DEFAULT_SEED_ENTROPY_BITS = 128;
public static final int MAX_SEED_ENTROPY_BITS = 512; public static final int MAX_SEED_ENTROPY_BITS = 512;
private final Type type;
private final byte[] seed; private final byte[] seed;
private final List<String> mnemonicCode; private final List<String> mnemonicCode;
@ -23,40 +25,45 @@ public class DeterministicSeed implements EncryptableItem {
private long creationTimeSeconds; private long creationTimeSeconds;
public DeterministicSeed(String mnemonicString, byte[] seed, String passphrase, long creationTimeSeconds) { //Session only storage
this(decodeMnemonicCode(mnemonicString), seed, passphrase, creationTimeSeconds); 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<String> mnemonic, long creationTimeSeconds) { public DeterministicSeed(byte[] seed, List<String> mnemonic, long creationTimeSeconds, Type type) {
this.seed = seed; this.seed = seed;
this.encryptedSeed = null; this.encryptedSeed = null;
this.mnemonicCode = mnemonic; this.mnemonicCode = mnemonic;
this.encryptedMnemonicCode = null; this.encryptedMnemonicCode = null;
this.creationTimeSeconds = creationTimeSeconds; 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.seed = null;
this.encryptedSeed = encryptedSeed; this.encryptedSeed = encryptedSeed;
this.mnemonicCode = null; this.mnemonicCode = null;
this.encryptedMnemonicCode = encryptedMnemonic; this.encryptedMnemonicCode = encryptedMnemonic;
this.creationTimeSeconds = creationTimeSeconds; 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. * details on this scheme.
* @param mnemonicCode A list of words. * @param mnemonicCode A list of words.
* @param seed The derived seed, or pass null to derive it from mnemonicCode (slow) * @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 passphrase A user supplied passphrase, or an empty string if there is no passphrase
* @param creationTimeSeconds When the seed was originally created, UNIX time. * @param creationTimeSeconds When the seed was originally created, UNIX time.
*/ */
public DeterministicSeed(List<String> mnemonicCode, byte[] seed, String passphrase, long creationTimeSeconds) { public DeterministicSeed(List<String> mnemonicCode, byte[] seed, String passphrase, long creationTimeSeconds, Type type) {
this((seed != null ? seed : Bip39MnemonicCode.toSeed(mnemonicCode, passphrase)), mnemonicCode, creationTimeSeconds); 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. * details on this scheme.
* @param random Entropy source * @param random Entropy source
* @param bits number of bits, must be divisible by 32 * @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. * details on this scheme.
* @param entropy entropy bits, length must be divisible by 32 * @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 * @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.encryptedMnemonicCode = null;
this.encryptedSeed = null; this.encryptedSeed = null;
this.creationTimeSeconds = creationTimeSeconds; this.creationTimeSeconds = creationTimeSeconds;
this.type = Type.BIP39;
} }
public boolean needPassphrase() { public boolean usesPassphrase() {
if(isEncrypted()) { if(isEncrypted()) {
throw new IllegalArgumentException("Cannot determine if passphrase is required in encrypted state"); 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); return Arrays.equals(mnemonicOnlySeed, seed);
} }
@ -172,6 +180,10 @@ public class DeterministicSeed implements EncryptableItem {
this.creationTimeSeconds = creationTimeSeconds; this.creationTimeSeconds = creationTimeSeconds;
} }
public Type getType() {
return type;
}
public DeterministicSeed encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) { public DeterministicSeed encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) {
if(encryptedSeed != null) { if(encryptedSeed != null) {
throw new IllegalArgumentException("Trying to encrypt seed twice"); 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 encryptedMnemonic = keyCrypter.encrypt(getMnemonicAsBytes(), null, aesKey);
EncryptedData encryptedSeed = keyCrypter.encrypt(seed, 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() { private byte[] getMnemonicAsBytes() {
@ -194,15 +206,15 @@ public class DeterministicSeed implements EncryptableItem {
} }
List<String> mnemonic = decodeMnemonicCode(crypter.decrypt(encryptedMnemonicCode, aesKey)); List<String> mnemonic = decodeMnemonicCode(crypter.decrypt(encryptedMnemonicCode, aesKey));
byte[] seed = encryptedSeed == null ? null : crypter.decrypt(encryptedSeed, 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) { public String getPassphrase() {
if(isEncrypted()) { return passphrase;
throw new UnsupportedOperationException("Cannot set passphrase on encrypted seed");
} }
return new DeterministicSeed(mnemonicCode, seed, passphrase, creationTimeSeconds); public void setPassphrase(String passphrase) {
this.passphrase = passphrase;
} }
@Override @Override
@ -227,13 +239,13 @@ public class DeterministicSeed implements EncryptableItem {
* @throws MnemonicException if check fails * @throws MnemonicException if check fails
*/ */
public void check() throws MnemonicException { public void check() throws MnemonicException {
if (mnemonicCode != null) { if(mnemonicCode != null) {
Bip39MnemonicCode.INSTANCE.check(mnemonicCode); type.check(mnemonicCode);
} }
} }
byte[] getEntropyBytes() throws MnemonicException { byte[] getEntropyBytes() throws MnemonicException {
return Bip39MnemonicCode.INSTANCE.toEntropy(mnemonicCode); return type.getEntropyBytes(mnemonicCode);
} }
/** Get the mnemonic code, or null if unknown. */ /** Get the mnemonic code, or null if unknown. */
@ -265,9 +277,44 @@ public class DeterministicSeed implements EncryptableItem {
public DeterministicSeed copy() { public DeterministicSeed copy() {
if(isEncrypted()) { 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<String> mnemonicCode) throws MnemonicException {
return Bip39MnemonicCode.INSTANCE.toEntropy(mnemonicCode);
}
public void check(List<String> mnemonicCode) throws MnemonicException {
Bip39MnemonicCode.INSTANCE.check(mnemonicCode);
}
public byte[] toSeed(List<String> mnemonicCode, String passphrase) {
return Bip39MnemonicCode.toSeed(mnemonicCode, passphrase);
}
},
ELECTRUM() {
public byte[] getEntropyBytes(List<String> mnemonicCode) throws MnemonicException {
throw new MnemonicException("Electrum seeds do not provide entropy bytes");
}
public void check(List<String> mnemonicCode) throws MnemonicException {
ElectrumMnemonicCode.INSTANCE.check(mnemonicCode);
}
public byte[] toSeed(List<String> mnemonicCode, String passphrase) {
return ElectrumMnemonicCode.toSeed(mnemonicCode, passphrase);
}
};
public abstract byte[] getEntropyBytes(List<String> mnemonicCode) throws MnemonicException;
public abstract void check(List<String> mnemonicCode) throws MnemonicException;
public abstract byte[] toSeed(List<String> mnemonicCode, String passphrase);
} }
} }

View file

@ -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<String> VALID_PREFIXES = List.of("01", "100", "101");
public static ElectrumMnemonicCode INSTANCE = new ElectrumMnemonicCode();
/**
* Gets the word list this code uses.
*/
public List<String> getWordList() {
return Bip39MnemonicCode.INSTANCE.getWordList();
}
public static byte[] toSeed(List<String> 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<String> words) throws MnemonicException {
String prefix = getPrefix(words);
if(!VALID_PREFIXES.contains(prefix)) {
throw new MnemonicException("Invalid prefix " + prefix);
}
}
private String getPrefix(List<String> 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");
}
}
}

View file

@ -172,14 +172,6 @@ public class Keystore {
return 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() { public boolean hasSeed() {
return seed != null; return seed != null;
} }