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;
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<ChildNumber> path) {
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 MAX_SEED_ENTROPY_BITS = 512;
private final Type type;
private final byte[] seed;
private final List<String> 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<String> mnemonic, long creationTimeSeconds) {
public DeterministicSeed(byte[] seed, List<String> 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<String> mnemonicCode, byte[] seed, String passphrase, long creationTimeSeconds) {
this((seed != null ? seed : Bip39MnemonicCode.toSeed(mnemonicCode, passphrase)), mnemonicCode, creationTimeSeconds);
public DeterministicSeed(List<String> 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<String> 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<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;
}
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;
}