diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java b/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java index 1422612..0cba865 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java @@ -7,10 +7,12 @@ import com.sparrowwallet.drongo.crypto.EncryptionType; import com.sparrowwallet.drongo.crypto.KeyCrypter; import org.bouncycastle.crypto.params.KeyParameter; +import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.StringJoiner; public class DeterministicSeed implements EncryptableItem { public static final int DEFAULT_SEED_ENTROPY_BITS = 128; @@ -18,20 +20,44 @@ public class DeterministicSeed implements EncryptableItem { private final byte[] seed; private final EncryptedData encryptedSeed; + + private final List mnemonicCode; + private final EncryptedData encryptedMnemonicCode; + private long creationTimeSeconds; - public DeterministicSeed(byte[] seed, long creationTimeSeconds) { + public DeterministicSeed(String mnemonicString, byte[] seed, String passphrase, long creationTimeSeconds) { + this(decodeMnemonicCode(mnemonicString), seed, passphrase, creationTimeSeconds); + } + + public DeterministicSeed(byte[] seed, List mnemonic, long creationTimeSeconds) { this.seed = seed; this.encryptedSeed = null; + this.mnemonicCode = mnemonic; + this.encryptedMnemonicCode = null; this.creationTimeSeconds = creationTimeSeconds; } - public DeterministicSeed(EncryptedData encryptedSeed, long creationTimeSeconds) { + public DeterministicSeed(EncryptedData encryptedMnemonic, EncryptedData encryptedSeed, long creationTimeSeconds) { this.seed = null; this.encryptedSeed = encryptedSeed; + this.mnemonicCode = null; + this.encryptedMnemonicCode = encryptedMnemonic; this.creationTimeSeconds = creationTimeSeconds; } + /** + * Constructs a seed from a BIP 39 mnemonic code. See {@link Bip39MnemonicCode} 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); + } + /** * Constructs a seed from a BIP 39 mnemonic code. See {@link Bip39MnemonicCode} for more * details on this scheme. @@ -63,14 +89,14 @@ public class DeterministicSeed implements EncryptableItem { passphrase = ""; } - List mnemonicCode; try { - mnemonicCode = Bip39MnemonicCode.INSTANCE.toMnemonic(entropy); + this.mnemonicCode = Bip39MnemonicCode.INSTANCE.toMnemonic(entropy); } catch (MnemonicException.MnemonicLengthException e) { // cannot happen throw new RuntimeException(e); } this.seed = Bip39MnemonicCode.toSeed(mnemonicCode, passphrase); + this.encryptedMnemonicCode = null; this.encryptedSeed = null; this.creationTimeSeconds = creationTimeSeconds; } @@ -87,15 +113,20 @@ public class DeterministicSeed implements EncryptableItem { @Override public boolean isEncrypted() { - return encryptedSeed != null; + if(mnemonicCode != null && encryptedMnemonicCode != null) { + throw new IllegalStateException("Cannot be in a encrypted and unencrypted state"); + } + + return encryptedMnemonicCode != null; } + @Override public String toString() { if(isEncrypted()) { return encryptedSeed.toString(); } - return Utils.bytesToHex(seed); + return toHexString(); } /** Returns the seed as hex or null if encrypted. */ @@ -105,7 +136,7 @@ public class DeterministicSeed implements EncryptableItem { @Override public byte[] getSecretBytes() { - return getSeedBytes(); + return getMnemonicAsBytes(); } public byte[] getSeedBytes() { @@ -114,7 +145,7 @@ public class DeterministicSeed implements EncryptableItem { @Override public EncryptedData getEncryptedData() { - return encryptedSeed; + return encryptedMnemonicCode; } @Override @@ -122,6 +153,10 @@ public class DeterministicSeed implements EncryptableItem { return EncryptionType.ENCRYPTED_SCRYPT_AES; } + public EncryptedData getEncryptedSeedData() { + return encryptedSeed; + } + @Override public long getCreationTimeSeconds() { return creationTimeSeconds; @@ -135,17 +170,25 @@ public class DeterministicSeed implements EncryptableItem { if(encryptedSeed != null) { throw new IllegalArgumentException("Trying to encrypt seed twice"); } - + if(mnemonicCode == null) { + throw new IllegalArgumentException("Mnemonic missing so cannot encrypt"); + } + EncryptedData encryptedMnemonic = keyCrypter.encrypt(getMnemonicAsBytes(), null, aesKey); EncryptedData encryptedSeed = keyCrypter.encrypt(seed, null, aesKey); - return new DeterministicSeed(encryptedSeed, creationTimeSeconds); + return new DeterministicSeed(encryptedMnemonic, encryptedSeed, creationTimeSeconds); + } + + private byte[] getMnemonicAsBytes() { + return getMnemonicString().getBytes(StandardCharsets.UTF_8); } public DeterministicSeed decrypt(KeyCrypter crypter, String passphrase, KeyParameter aesKey) { if(!isEncrypted()) { throw new IllegalStateException("Cannot decrypt unencrypted seed"); } - byte[] seed = crypter.decrypt(encryptedSeed, aesKey); - return new DeterministicSeed(seed, passphrase, creationTimeSeconds); + List mnemonic = decodeMnemonicCode(crypter.decrypt(encryptedMnemonicCode, aesKey)); + byte[] seed = encryptedSeed == null ? null : crypter.decrypt(encryptedSeed, aesKey); + return new DeterministicSeed(mnemonic, seed, passphrase, creationTimeSeconds); } @Override @@ -154,11 +197,55 @@ public class DeterministicSeed implements EncryptableItem { if (o == null || getClass() != o.getClass()) return false; DeterministicSeed other = (DeterministicSeed) o; return creationTimeSeconds == other.creationTimeSeconds - && (isEncrypted() ? encryptedSeed.equals(other.encryptedSeed) : Arrays.equals(seed, other.seed)); + && Objects.equals(encryptedMnemonicCode, other.encryptedMnemonicCode) + && Objects.equals(mnemonicCode, other.mnemonicCode); } @Override public int hashCode() { - return Objects.hash(creationTimeSeconds, isEncrypted() ? encryptedSeed : seed); + return Objects.hash(creationTimeSeconds, encryptedMnemonicCode, mnemonicCode); + } + + /** + * Check if our mnemonic is a valid mnemonic phrase for our word list. + * Does nothing if we are encrypted. + * + * @throws MnemonicException if check fails + */ + public void check() throws MnemonicException { + if (mnemonicCode != null) { + Bip39MnemonicCode.INSTANCE.check(mnemonicCode); + } + } + + byte[] getEntropyBytes() throws MnemonicException { + return Bip39MnemonicCode.INSTANCE.toEntropy(mnemonicCode); + } + + /** Get the mnemonic code, or null if unknown. */ + public List getMnemonicCode() { + return mnemonicCode; + } + + /** Get the mnemonic code as string, or null if unknown. */ + public String getMnemonicString() { + StringJoiner joiner = new StringJoiner(" "); + if(mnemonicCode != null) { + for(String word : mnemonicCode) { + joiner.add(word); + } + + return joiner.toString(); + } + + return null; + } + + private static List decodeMnemonicCode(byte[] mnemonicCode) { + return decodeMnemonicCode(new String(mnemonicCode, StandardCharsets.UTF_8)); + } + + private static List decodeMnemonicCode(String mnemonicCode) { + return Arrays.asList(mnemonicCode.split(" ")); } }