mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-27 02:26:44 +00:00
electrum seed version system support
This commit is contained in:
parent
6b1f7d0174
commit
9e5a7d0e8d
4 changed files with 150 additions and 35 deletions
|
@ -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("/");
|
||||||
|
|
|
@ -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
|
||||||
|
@ -228,12 +240,12 @@ public class DeterministicSeed implements EncryptableItem {
|
||||||
*/
|
*/
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue