mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-26 10:06:45 +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;
|
||||
|
||||
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("/");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue