rework deterministicseed, keycrypter

This commit is contained in:
Craig Raw 2020-05-16 15:09:15 +02:00
parent 9e5a7d0e8d
commit 312143cb61
17 changed files with 221 additions and 174 deletions

View file

@ -1,9 +1,6 @@
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.crypto.*;
import com.sparrowwallet.drongo.protocol.ProtocolException;
import com.sparrowwallet.drongo.protocol.Ripemd160;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
@ -258,8 +255,8 @@ public class Utils {
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));
EncryptedData data = new EncryptedData(initializationVector, encryptedBytes, null);
return keyCrypter.decrypt(data, new Key(keyBytes, null));
}
/** Convert to a string path, starting with "M/" */

View file

@ -29,7 +29,7 @@ public class AESKeyCrypter implements KeyCrypter {
}
@Override
public KeyParameter deriveKey(CharSequence password) throws KeyCrypterException {
public Key deriveKey(CharSequence password) throws KeyCrypterException {
throw new UnsupportedOperationException("AESKeyCrypter does not define a key derivation function, but keys must be either 128, 192 or 256 bits long");
}
@ -42,13 +42,13 @@ public class AESKeyCrypter implements KeyCrypter {
* @throws KeyCrypterException if bytes could not be decrypted
*/
@Override
public byte[] decrypt(EncryptedData dataToDecrypt, KeyParameter aesKey) throws KeyCrypterException {
public byte[] decrypt(EncryptedData dataToDecrypt, Key aesKey) throws KeyCrypterException {
if(dataToDecrypt == null || aesKey == null) {
throw new KeyCrypterException("Data and key to decrypt cannot be null");
}
try {
ParametersWithIV keyWithIv = new ParametersWithIV(new KeyParameter(aesKey.getKey()), dataToDecrypt.getInitialisationVector());
ParametersWithIV keyWithIv = new ParametersWithIV(new KeyParameter(aesKey.getKeyBytes()), dataToDecrypt.getInitialisationVector());
// Decrypt the message.
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
@ -71,7 +71,7 @@ public class AESKeyCrypter implements KeyCrypter {
* Password based encryption using AES - CBC - PKCS7
*/
@Override
public EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, KeyParameter aesKey) throws KeyCrypterException {
public EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, Key aesKey) throws KeyCrypterException {
if(plainBytes == null || aesKey == null) {
throw new KeyCrypterException("Data and key to encrypt cannot be null");
}
@ -84,7 +84,7 @@ public class AESKeyCrypter implements KeyCrypter {
secureRandom.nextBytes(iv);
}
ParametersWithIV keyWithIv = new ParametersWithIV(aesKey, iv);
ParametersWithIV keyWithIv = new ParametersWithIV(new KeyParameter(aesKey.getKeyBytes()), iv);
// Encrypt using AES.
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
@ -93,7 +93,7 @@ public class AESKeyCrypter implements KeyCrypter {
final int length1 = cipher.processBytes(plainBytes, 0, plainBytes.length, encryptedBytes, 0);
final int length2 = cipher.doFinal(encryptedBytes, length1);
return new EncryptedData(iv, Arrays.copyOf(encryptedBytes, length1 + length2));
return new EncryptedData(iv, Arrays.copyOf(encryptedBytes, length1 + length2), aesKey.getSalt());
} catch (Exception e) {
throw new KeyCrypterException("Could not encrypt bytes.", e);
}

View file

@ -8,9 +8,9 @@ public interface AsymmetricKeyCrypter {
EncryptionType getUnderstoodEncryptionType();
/**
* Create a KeyParameter (which typically contains an AES key)
* Create a ECKey based on the provided password
* @param password
* @return KeyParameter The KeyParameter which typically contains the AES key to use for encrypting and decrypting
* @return ECKey The ECKey to use for encrypting and decrypting
* @throws KeyCrypterException
*/
ECKey deriveECKey(CharSequence password) throws KeyCrypterException;

View file

@ -65,13 +65,13 @@ public class ECIESKeyCrypter implements AsymmetricKeyCrypter {
throw new InvalidPasswordException();
}
return aesKeyCrypter.decrypt(new EncryptedData(iv, ciphertext), new KeyParameter(key_e));
return aesKeyCrypter.decrypt(new EncryptedData(iv, ciphertext, null), new Key(key_e, null));
}
@Override
public EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, ECKey key) throws KeyCrypterException {
byte[] encryptedBytes = encryptEcies(key, plainBytes, initializationVector);
return new EncryptedData(initializationVector, encryptedBytes);
return new EncryptedData(initializationVector, encryptedBytes, null);
}
public byte[] encryptEcies(ECKey key, byte[] message, byte[] magic) {
@ -83,7 +83,7 @@ public class ECIESKeyCrypter implements AsymmetricKeyCrypter {
byte[] key_e = Arrays.copyOfRange(hash, 16, 32);
byte[] key_m = Arrays.copyOfRange(hash, 32, 64);
byte[] ciphertext = aesKeyCrypter.encrypt(message, iv, new KeyParameter(key_e)).getEncryptedBytes();
byte[] ciphertext = aesKeyCrypter.encrypt(message, iv, new Key(key_e, null)).getEncryptedBytes();
byte[] encrypted = concat(magic, ephemeral.getPubKey(), ciphertext);
byte[] result = hmac256(key_m, encrypted);
return Base64.getEncoder().encode(concat(encrypted, result));

View file

@ -7,14 +7,11 @@ import org.bouncycastle.asn1.*;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.ec.CustomNamedCurves;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.*;
import org.bouncycastle.crypto.signers.ECDSASigner;
import org.bouncycastle.crypto.signers.HMacDSAKCalculator;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
import org.bouncycastle.math.ec.FixedPointUtil;
@ -499,7 +496,7 @@ public class ECKey implements EncryptableItem {
* @throws KeyCrypterException if there's something wrong with aesKey.
* @throws ECKey.MissingPrivateKeyException if this key cannot sign because it's pubkey only.
*/
public ECDSASignature sign(Sha256Hash input, KeyParameter aesKey) throws KeyCrypterException {
public ECDSASignature sign(Sha256Hash input, Key aesKey) throws KeyCrypterException {
KeyCrypter crypter = getKeyCrypter();
if (crypter != null) {
if (aesKey == null) {
@ -727,10 +724,10 @@ public class ECKey implements EncryptableItem {
* This method returns a new encrypted key and leaves the original unchanged.
*
* @param keyCrypter The keyCrypter that specifies exactly how the encrypted bytes are created.
* @param aesKey The KeyParameter with the AES encryption key (usually constructed with keyCrypter#deriveKey and cached as it is slow to create).
* @param aesKey The Key with the AES encryption key (usually constructed with keyCrypter#deriveKey and cached as it is slow to create).
* @return encryptedKey
*/
public ECKey encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) throws KeyCrypterException {
public ECKey encrypt(KeyCrypter keyCrypter, Key aesKey) throws KeyCrypterException {
if(keyCrypter == null) {
throw new KeyCrypterException("Keycrypter cannot be null");
}
@ -748,9 +745,9 @@ public class ECKey implements EncryptableItem {
* just yield a garbage key.
*
* @param keyCrypter The keyCrypter that specifies exactly how the decrypted bytes are created.
* @param aesKey The KeyParameter with the AES encryption key (usually constructed with keyCrypter#deriveKey and cached).
* @param aesKey The Key with the AES encryption key (usually constructed with keyCrypter#deriveKey and cached).
*/
public ECKey decrypt(KeyCrypter keyCrypter, KeyParameter aesKey) throws KeyCrypterException {
public ECKey decrypt(KeyCrypter keyCrypter, Key aesKey) throws KeyCrypterException {
if(keyCrypter == null) {
throw new KeyCrypterException("Keycrypter cannot be null");
}
@ -780,9 +777,9 @@ public class ECKey implements EncryptableItem {
* has some chance of throwing KeyCrypterException due to the corrupted padding that will result, but it can also
* just yield a garbage key.
*
* @param aesKey The KeyParameter with the AES encryption key (usually constructed with keyCrypter#deriveKey and cached).
* @param aesKey The Key with the AES encryption key (usually constructed with keyCrypter#deriveKey and cached).
*/
public ECKey decrypt(KeyParameter aesKey) throws KeyCrypterException {
public ECKey decrypt(Key aesKey) throws KeyCrypterException {
final KeyCrypter crypter = getKeyCrypter();
if (crypter == null) {
throw new KeyCrypterException("No key crypter available");
@ -794,7 +791,7 @@ public class ECKey implements EncryptableItem {
/**
* Creates decrypted private key if needed.
*/
public ECKey maybeDecrypt(KeyParameter aesKey) throws KeyCrypterException {
public ECKey maybeDecrypt(Key aesKey) throws KeyCrypterException {
return isEncrypted() && aesKey != null ? decrypt(aesKey) : this;
}
@ -807,7 +804,7 @@ public class ECKey implements EncryptableItem {
*
* @return true if the encrypted key can be decrypted back to the original key successfully.
*/
public static boolean encryptionIsReversible(ECKey originalKey, ECKey encryptedKey, KeyCrypter keyCrypter, KeyParameter aesKey) {
public static boolean encryptionIsReversible(ECKey originalKey, ECKey encryptedKey, KeyCrypter keyCrypter, Key aesKey) {
try {
ECKey rebornUnencryptedKey = encryptedKey.decrypt(keyCrypter, aesKey);
byte[] originalPrivateKeyBytes = originalKey.getPrivKeyBytes();

View file

@ -13,10 +13,12 @@ import java.util.Objects;
public final class EncryptedData {
private final byte[] initialisationVector;
private final byte[] encryptedBytes;
private final byte[] keySalt;
public EncryptedData(byte[] initialisationVector, byte[] encryptedBytes) {
public EncryptedData(byte[] initialisationVector, byte[] encryptedBytes, byte[] keySalt) {
this.initialisationVector = Arrays.copyOf(initialisationVector, initialisationVector.length);
this.encryptedBytes = Arrays.copyOf(encryptedBytes, encryptedBytes.length);
this.keySalt = keySalt == null ? null : Arrays.copyOf(keySalt, keySalt.length);
}
public byte[] getInitialisationVector() {
@ -27,27 +29,33 @@ public final class EncryptedData {
return encryptedBytes;
}
public byte[] getKeySalt() {
return keySalt;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EncryptedData other = (EncryptedData) o;
return Arrays.equals(encryptedBytes, other.encryptedBytes) && Arrays.equals(initialisationVector, other.initialisationVector);
return Arrays.equals(encryptedBytes, other.encryptedBytes) && Arrays.equals(initialisationVector, other.initialisationVector) && Arrays.equals(keySalt, other.keySalt);
}
@Override
public int hashCode() {
return Objects.hash(Arrays.hashCode(encryptedBytes), Arrays.hashCode(initialisationVector));
return Objects.hash(Arrays.hashCode(encryptedBytes), Arrays.hashCode(initialisationVector), Arrays.hashCode(keySalt));
}
@Override
public String toString() {
return "EncryptedData [initialisationVector=" + Arrays.toString(initialisationVector)
+ ", encryptedPrivateKey=" + Arrays.toString(encryptedBytes) + "]";
+ ", encryptedPrivateKey=" + Arrays.toString(encryptedBytes)
+ ", keySalt=" + Arrays.toString(keySalt) + "]";
}
public EncryptedData copy() {
return new EncryptedData(Arrays.copyOf(initialisationVector, initialisationVector.length),
Arrays.copyOf(encryptedBytes, encryptedBytes.length));
Arrays.copyOf(encryptedBytes, encryptedBytes.length),
Arrays.copyOf(keySalt, keySalt.length));
}
}

View file

@ -0,0 +1,19 @@
package com.sparrowwallet.drongo.crypto;
public class Key {
private final byte[] keyBytes;
private final byte[] salt;
public Key(byte[] keyBytes, byte[] salt) {
this.keyBytes = keyBytes;
this.salt = salt;
}
public byte[] getKeyBytes() {
return keyBytes;
}
public byte[] getSalt() {
return salt;
}
}

View file

@ -1,18 +1,14 @@
package com.sparrowwallet.drongo.crypto;
import org.bouncycastle.crypto.params.KeyParameter;
import java.io.Serializable;
/**
* <p>A KeyCrypter can be used to encrypt and decrypt a message. The sequence of events to encrypt and then decrypt
* a message are as follows:</p>
*
* <p>(1) Ask the user for a password. deriveKey() is then called to create an KeyParameter. This contains the AES
* <p>(1) Ask the user for a password. deriveKey() is then called to create an Key. This contains the AES
* key that will be used for encryption.</p>
* <p>(2) Encrypt the message using encrypt(), providing the message bytes and the KeyParameter from (1). This returns
* <p>(2) Encrypt the message using encrypt(), providing the message bytes and the Key from (1). This returns
* an EncryptedData which contains the encryptedPrivateKey bytes and an initialisation vector.</p>
* <p>(3) To decrypt an EncryptedData, repeat step (1) to get a KeyParameter, then call decrypt().</p>
* <p>(3) To decrypt an EncryptedData, repeat step (1) to get a Key, then call decrypt().</p>
*
* <p>There can be different algorithms used for encryption/ decryption so the getUnderstoodEncryptionType is used
* to determine whether any given KeyCrypter can understand the type of encrypted data you have.</p>
@ -26,19 +22,19 @@ public interface KeyCrypter {
EncryptionType getUnderstoodEncryptionType();
/**
* Create a KeyParameter (which typically contains an AES key)
* Create a Key (which typically contains an AES key)
* @param password
* @return KeyParameter The KeyParameter which typically contains the AES key to use for encrypting and decrypting
* @return Key The Key which typically contains the AES key to use for encrypting and decrypting
* @throws KeyCrypterException
*/
KeyParameter deriveKey(CharSequence password) throws KeyCrypterException;
Key deriveKey(CharSequence password) throws KeyCrypterException;
/**
* Decrypt the provided encrypted bytes, converting them into unencrypted bytes.
*
* @throws KeyCrypterException if decryption was unsuccessful.
*/
byte[] decrypt(EncryptedData encryptedBytesToDecode, KeyParameter key) throws KeyCrypterException;
byte[] decrypt(EncryptedData encryptedBytesToDecode, Key key) throws KeyCrypterException;
/**
* Encrypt the supplied bytes, converting them into ciphertext.
@ -46,5 +42,5 @@ public interface KeyCrypter {
* @return encryptedPrivateKey An encryptedPrivateKey containing the encrypted bytes and an initialisation vector.
* @throws KeyCrypterException if encryption was unsuccessful
*/
EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, KeyParameter key) throws KeyCrypterException;
EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, Key key) throws KeyCrypterException;
}

View file

@ -1,7 +1,6 @@
package com.sparrowwallet.drongo.crypto;
import org.bouncycastle.crypto.generators.SCrypt;
import org.bouncycastle.crypto.params.KeyParameter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -54,6 +53,13 @@ public class ScryptKeyCrypter extends AESKeyCrypter {
this.scryptParameters = new ScryptParameters(randomSalt());
}
/**
* Encryption/Decryption using default parameters and provided salt.
*/
public ScryptKeyCrypter(byte[] salt) {
this.scryptParameters = new ScryptParameters(salt);
}
/**
* Encryption/Decryption using custom number of iterations parameters and a random salt.
* As of August 2016, a useful value for mobile devices is 4096 (derivation takes about 1 second).
@ -84,11 +90,11 @@ public class ScryptKeyCrypter extends AESKeyCrypter {
* This is a very slow operation compared to encrypt/ decrypt so it is normally worth caching the result.
*
* @param password The password to use in key generation
* @return The KeyParameter containing the created AES key
* @return The Key containing the created AES key
* @throws KeyCrypterException
*/
@Override
public KeyParameter deriveKey(CharSequence password) throws KeyCrypterException {
public Key deriveKey(CharSequence password) throws KeyCrypterException {
byte[] passwordBytes = null;
try {
passwordBytes = convertToByteArray(password);
@ -100,7 +106,7 @@ public class ScryptKeyCrypter extends AESKeyCrypter {
}
byte[] keyBytes = SCrypt.generate(passwordBytes, salt, (int) scryptParameters.getN(), scryptParameters.getR(), scryptParameters.getP(), KEY_LENGTH);
return new KeyParameter(keyBytes);
return new Key(keyBytes, scryptParameters.getSalt());
} catch (Exception e) {
throw new KeyCrypterException("Could not generate key from password and salt.", e);
} finally {

View file

@ -95,7 +95,7 @@ public class Bip39MnemonicCode {
*/
public static byte[] toSeed(List<String> words, String passphrase) {
if(passphrase == null) {
throw new IllegalArgumentException("A null passphrase is not allowed.");
passphrase = "";
}
// To create binary seed from mnemonic, we use PBKDF2 function

View file

@ -1,11 +1,7 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.EncryptableItem;
import com.sparrowwallet.drongo.crypto.EncryptedData;
import com.sparrowwallet.drongo.crypto.EncryptionType;
import com.sparrowwallet.drongo.crypto.KeyCrypter;
import org.bouncycastle.crypto.params.KeyParameter;
import com.sparrowwallet.drongo.crypto.*;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
@ -16,52 +12,39 @@ public class DeterministicSeed implements EncryptableItem {
public static final int MAX_SEED_ENTROPY_BITS = 512;
private final Type type;
private final byte[] seed;
private final List<String> mnemonicCode;
private final EncryptedData encryptedSeed;
private final EncryptedData encryptedMnemonicCode;
private final boolean needsPassphrase;
private long 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(String mnemonicString, String passphrase, long creationTimeSeconds, Type type) {
this(decodeMnemonicCode(mnemonicString), passphrase, creationTimeSeconds, type);
}
public DeterministicSeed(byte[] seed, List<String> mnemonic, long creationTimeSeconds, Type type) {
this.seed = seed;
this.encryptedSeed = null;
public DeterministicSeed(List<String> mnemonic, String passphrase, long creationTimeSeconds, Type type) {
this(mnemonic, needsPassphrase(passphrase), creationTimeSeconds, type);
this.passphrase = passphrase;
}
public DeterministicSeed(List<String> mnemonic, boolean needsPassphrase, long creationTimeSeconds, Type type) {
this.mnemonicCode = mnemonic;
this.encryptedMnemonicCode = null;
this.needsPassphrase = needsPassphrase;
this.creationTimeSeconds = creationTimeSeconds;
this.type = type;
}
public DeterministicSeed(EncryptedData encryptedMnemonic, EncryptedData encryptedSeed, long creationTimeSeconds, Type type) {
this.seed = null;
this.encryptedSeed = encryptedSeed;
public DeterministicSeed(EncryptedData encryptedMnemonic, boolean needsPassphrase, long creationTimeSeconds, Type type) {
this.mnemonicCode = null;
this.encryptedMnemonicCode = encryptedMnemonic;
this.needsPassphrase = needsPassphrase;
this.creationTimeSeconds = creationTimeSeconds;
this.type = type;
}
/**
* 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, Type type) {
this((seed != null ? seed : type.toSeed(mnemonicCode, passphrase)), mnemonicCode, creationTimeSeconds, type);
}
/**
* Constructs a new BIP39 seed. See {@link Bip39MnemonicCode} for more
* details on this scheme.
@ -99,20 +82,26 @@ public class DeterministicSeed implements EncryptableItem {
// cannot happen
throw new RuntimeException(e);
}
this.seed = Bip39MnemonicCode.toSeed(mnemonicCode, passphrase);
this.encryptedMnemonicCode = null;
this.encryptedSeed = null;
this.needsPassphrase = needsPassphrase(passphrase);
this.creationTimeSeconds = creationTimeSeconds;
this.type = Type.BIP39;
}
public boolean usesPassphrase() {
if(isEncrypted()) {
throw new IllegalArgumentException("Cannot determine if passphrase is required in encrypted state");
public static boolean needsPassphrase(String passphrase) {
return passphrase != null && !passphrase.isEmpty();
}
byte[] mnemonicOnlySeed = type.toSeed(mnemonicCode, "");
return Arrays.equals(mnemonicOnlySeed, seed);
public boolean needsPassphrase() {
return needsPassphrase;
}
public String getPassphrase() {
return passphrase;
}
public void setPassphrase(String passphrase) {
this.passphrase = passphrase;
}
private static byte[] getEntropy(SecureRandom random, int bits) {
@ -137,14 +126,15 @@ public class DeterministicSeed implements EncryptableItem {
@Override
public String toString() {
if(isEncrypted()) {
return encryptedSeed.toString();
return encryptedMnemonicCode.toString();
}
return toHexString();
return getMnemonicString();
}
/** Returns the seed as hex or null if encrypted. */
public String toHexString() {
public String toHexString() throws MnemonicException {
byte[] seed = getSeedBytes();
return seed != null ? Utils.bytesToHex(seed) : null;
}
@ -153,8 +143,12 @@ public class DeterministicSeed implements EncryptableItem {
return getMnemonicAsBytes();
}
public byte[] getSeedBytes() {
return seed;
public byte[] getSeedBytes() throws MnemonicException {
if(passphrase == null && needsPassphrase) {
throw new MnemonicException("Passphrase required but not provided");
}
return type.toSeed(mnemonicCode, passphrase);
}
@Override
@ -167,10 +161,6 @@ public class DeterministicSeed implements EncryptableItem {
return EncryptionType.ENCRYPTED_SCRYPT_AES;
}
public EncryptedData getEncryptedSeedData() {
return encryptedSeed;
}
@Override
public long getCreationTimeSeconds() {
return creationTimeSeconds;
@ -184,37 +174,27 @@ public class DeterministicSeed implements EncryptableItem {
return type;
}
public DeterministicSeed encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) {
if(encryptedSeed != null) {
throw new IllegalArgumentException("Trying to encrypt seed twice");
public DeterministicSeed encrypt(KeyCrypter keyCrypter, Key aesKey) {
if(encryptedMnemonicCode != null) {
throw new IllegalArgumentException("Trying to encrypt 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(encryptedMnemonic, encryptedSeed, creationTimeSeconds, type);
return new DeterministicSeed(encryptedMnemonic, needsPassphrase, creationTimeSeconds, type);
}
private byte[] getMnemonicAsBytes() {
return getMnemonicString().getBytes(StandardCharsets.UTF_8);
}
public DeterministicSeed decrypt(KeyCrypter crypter, String passphrase, KeyParameter aesKey) {
public DeterministicSeed decrypt(KeyCrypter crypter, Key aesKey) {
if(!isEncrypted()) {
throw new IllegalStateException("Cannot decrypt unencrypted seed");
}
List<String> mnemonic = decodeMnemonicCode(crypter.decrypt(encryptedMnemonicCode, aesKey));
byte[] seed = encryptedSeed == null ? null : crypter.decrypt(encryptedSeed, aesKey);
return new DeterministicSeed(mnemonic, seed, passphrase, creationTimeSeconds, type);
}
public String getPassphrase() {
return passphrase;
}
public void setPassphrase(String passphrase) {
this.passphrase = passphrase;
return new DeterministicSeed(mnemonic, needsPassphrase, creationTimeSeconds, type);
}
@Override
@ -276,11 +256,16 @@ public class DeterministicSeed implements EncryptableItem {
}
public DeterministicSeed copy() {
DeterministicSeed seed;
if(isEncrypted()) {
return new DeterministicSeed(encryptedMnemonicCode.copy(), encryptedSeed.copy(), creationTimeSeconds, type);
seed = new DeterministicSeed(encryptedMnemonicCode.copy(), needsPassphrase, creationTimeSeconds, type);
} else {
seed = new DeterministicSeed(new ArrayList<>(mnemonicCode), needsPassphrase, creationTimeSeconds, type);
}
return new DeterministicSeed(Arrays.copyOf(seed, seed.length), new ArrayList<>(mnemonicCode), creationTimeSeconds, type);
seed.setPassphrase(passphrase);
return seed;
}
public enum Type {

View file

@ -4,7 +4,6 @@ import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.*;
import org.bouncycastle.crypto.params.KeyParameter;
import java.util.List;
@ -78,7 +77,7 @@ public class Keystore {
this.seed = seed;
}
public DeterministicKey getMasterPrivateKey() {
public DeterministicKey getMasterPrivateKey() throws MnemonicException {
if(seed == null) {
throw new IllegalArgumentException("Keystore does not contain a seed");
}
@ -90,15 +89,15 @@ public class Keystore {
return HDKeyDerivation.createMasterPrivateKey(seed.getSeedBytes());
}
public ExtendedKey getExtendedMasterPrivateKey() {
public ExtendedKey getExtendedMasterPrivateKey() throws MnemonicException {
return new ExtendedKey(getMasterPrivateKey(), new byte[4], ChildNumber.ZERO);
}
public ExtendedKey getExtendedMasterPublicKey() {
public ExtendedKey getExtendedMasterPublicKey() throws MnemonicException {
return new ExtendedKey(getMasterPrivateKey().dropPrivateBytes(), new byte[4], ChildNumber.ZERO);
}
public ExtendedKey getExtendedPrivateKey() {
public ExtendedKey getExtendedPrivateKey() throws MnemonicException {
List<ChildNumber> derivation = getKeyDerivation().getDerivation();
DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation);
return new ExtendedKey(derivedKey, derivedKey.getParentFingerprint(), derivation.get(derivation.size() - 1));
@ -126,6 +125,8 @@ public class Keystore {
return false;
}
if(!seed.isEncrypted()) {
try {
List<ChildNumber> derivation = getKeyDerivation().getDerivation();
DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation);
DeterministicKey derivedKeyPublicOnly = derivedKey.dropPrivateBytes().dropParent();
@ -133,6 +134,10 @@ public class Keystore {
if(!xpub.equals(getExtendedPublicKey())) {
return false;
}
} catch(MnemonicException e) {
return false;
}
}
}
return true;
@ -154,7 +159,7 @@ public class Keystore {
return copy;
}
public static Keystore fromSeed(DeterministicSeed seed, List<ChildNumber> derivation) {
public static Keystore fromSeed(DeterministicSeed seed, List<ChildNumber> derivation) throws MnemonicException {
Keystore keystore = new Keystore();
keystore.setSeed(seed);
ExtendedKey xprv = keystore.getExtendedMasterPrivateKey();
@ -181,24 +186,16 @@ public class Keystore {
}
public void encrypt(String password) {
KeyCrypter keyCrypter = new ScryptKeyCrypter();
encrypt(keyCrypter, keyCrypter.deriveKey(password));
}
public void encrypt(KeyCrypter keyCrypter, KeyParameter key) {
if(seed != null && !seed.isEncrypted()) {
seed = seed.encrypt(keyCrypter, key);
}
}
public void decrypt(String password, String passphrase) {
KeyCrypter keyCrypter = new ScryptKeyCrypter();
decrypt(keyCrypter, passphrase, keyCrypter.deriveKey(password));
seed = seed.encrypt(keyCrypter, keyCrypter.deriveKey(password));
}
}
public void decrypt(KeyCrypter keyCrypter, String passphrase, KeyParameter key) {
public void decrypt(String password) {
if(seed != null && seed.isEncrypted()) {
seed = seed.decrypt(keyCrypter, passphrase, key);
KeyCrypter keyCrypter = new ScryptKeyCrypter(seed.getEncryptedData().getKeySalt());
seed = seed.decrypt(keyCrypter, keyCrypter.deriveKey(password));
}
}
}

View file

@ -1,11 +1,8 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.crypto.KeyCrypter;
import com.sparrowwallet.drongo.crypto.ScryptKeyCrypter;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import org.bouncycastle.crypto.params.KeyParameter;
import java.util.*;
@ -139,6 +136,16 @@ public class Wallet {
return copy;
}
public boolean containsSeeds() {
for(Keystore keystore : keystores) {
if(keystore.hasSeed()) {
return true;
}
}
return false;
}
public boolean isEncrypted() {
for(Keystore keystore : keystores) {
if(keystore.isEncrypted()) {
@ -150,18 +157,14 @@ public class Wallet {
}
public void encrypt(String password) {
KeyCrypter keyCrypter = new ScryptKeyCrypter();
KeyParameter key = keyCrypter.deriveKey(password);
for(Keystore keystore : keystores) {
keystore.encrypt(keyCrypter, key);
keystore.encrypt(password);
}
}
public void decrypt(String password, String passphrase) {
KeyCrypter keyCrypter = new ScryptKeyCrypter();
KeyParameter key = keyCrypter.deriveKey(password);
public void decrypt(String password) {
for(Keystore keystore : keystores) {
keystore.decrypt(keyCrypter, passphrase, key);
keystore.decrypt(password);
}
}
}

View file

@ -1,36 +1,33 @@
package com.sparrowwallet.drongo.crypto;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Assert;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.Security;
public class ScryptKeyCrypterTest {
@Test
public void testScrypt() {
ScryptKeyCrypter scryptKeyCrypter = new ScryptKeyCrypter();
KeyParameter keyParameter = scryptKeyCrypter.deriveKey("password");
Key key = scryptKeyCrypter.deriveKey("pass");
String message = "testastringmessage";
String message = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor";
byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
byte[] iv = new byte[16];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(iv);
EncryptedData scrypted = scryptKeyCrypter.encrypt(messageBytes, iv, keyParameter);
EncryptedData scrypted = scryptKeyCrypter.encrypt(messageBytes, iv, key);
AESKeyCrypter aesKeyCrypter = new AESKeyCrypter();
EncryptedData aescrypted = aesKeyCrypter.encrypt(messageBytes, iv, keyParameter);
EncryptedData aescrypted = aesKeyCrypter.encrypt(messageBytes, iv, key);
Assert.assertArrayEquals(scrypted.getEncryptedBytes(), aescrypted.getEncryptedBytes());
byte[] sdecrypted = scryptKeyCrypter.decrypt(scrypted, keyParameter);
byte[] aesdecrypted = aesKeyCrypter.decrypt(aescrypted, keyParameter);
byte[] sdecrypted = scryptKeyCrypter.decrypt(scrypted, key);
byte[] aesdecrypted = aesKeyCrypter.decrypt(aescrypted, key);
Assert.assertArrayEquals(sdecrypted, aesdecrypted);

View file

@ -0,0 +1,28 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.Key;
import com.sparrowwallet.drongo.crypto.KeyCrypter;
import com.sparrowwallet.drongo.crypto.ScryptKeyCrypter;
import org.junit.Assert;
import org.junit.Test;
public class DeterministicSeedTest {
@Test
public void testEncryption() {
String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor";
KeyCrypter keyCrypter = new ScryptKeyCrypter();
Key key = keyCrypter.deriveKey("pass");
DeterministicSeed seed = new DeterministicSeed(words, "pp", 0, DeterministicSeed.Type.BIP39);
DeterministicSeed encryptedSeed = seed.encrypt(keyCrypter, key);
System.out.println(Utils.bytesToHex(encryptedSeed.getEncryptedData().getInitialisationVector()));
System.out.println(Utils.bytesToHex(encryptedSeed.getEncryptedData().getEncryptedBytes()));
KeyCrypter keyCrypter2 = new ScryptKeyCrypter();
Key key2 = keyCrypter2.deriveKey("pass");
seed = encryptedSeed.decrypt(keyCrypter2, key2);
Assert.assertEquals(words, seed.getMnemonicString());
}
}

View file

@ -9,29 +9,20 @@ import java.util.Collections;
public class KeystoreTest {
@Test
public void testExtendedPrivateKey() {
public void testExtendedPrivateKey() throws MnemonicException {
Keystore keystore = new Keystore();
DeterministicSeed seed = new DeterministicSeed(Utils.hexToBytes("727ecfcf0bce9d8ec0ef066f7aeb845c271bdd4ee06a37398cebd40dc810140bb620b6c10a8ad671afdceaf37aa55d92d6478f747e8b92430dd938ab5be961dd"), Collections.emptyList(), 0);
DeterministicSeed seed = new DeterministicSeed("absent essay fox snake vast pumpkin height crouch silent bulb excuse razor", "", 0, DeterministicSeed.Type.BIP39);
keystore.setSeed(seed);
Assert.assertEquals("xprv9s21ZrQH143K3rN5vhm4bKDKsk1PmUK1mzxSMwkVSp2GbomwGmjLaGqrs8Nn9r14jCsfCNWfTR6pAtCsJutUH6QSHX65CePNW3YVyGxqvJa", keystore.getExtendedMasterPrivateKey().toString());
}
@Test
public void testExtendedPrivateKeyTwo() {
Keystore keystore = new Keystore();
DeterministicSeed seed = new DeterministicSeed(Utils.hexToBytes("4d8c47d0d6241169d0b17994219211c4a980f7146bb70dbc897416790e9de23a6265c708b88176e24f6eb7378a7c55cd4bdc067cafe574eaf3480f9a41c3c43c"), Collections.emptyList(), 0);
keystore.setSeed(seed);
Assert.assertEquals("xprv9s21ZrQH143K4AkrxAyivDeTCWhZV6fdLfBRR8QerWe9hHiRqMjBMj9MFNef7oFufgcDcW54LhguPNm6MVLEMWPX5qxKhmNjCzi9Zy6yhkc", keystore.getExtendedMasterPrivateKey().toString());
}
@Test
public void testFromSeed() {
public void testFromSeed() throws MnemonicException {
ScriptType p2pkh = ScriptType.P2PKH;
DeterministicSeed seed = new DeterministicSeed(Utils.hexToBytes("4d8c47d0d6241169d0b17994219211c4a980f7146bb70dbc897416790e9de23a6265c708b88176e24f6eb7378a7c55cd4bdc067cafe574eaf3480f9a41c3c43c"), Collections.emptyList(), 0);
DeterministicSeed seed = new DeterministicSeed("absent essay fox snake vast pumpkin height crouch silent bulb excuse razor", "", 0, DeterministicSeed.Type.BIP39);
Keystore keystore = Keystore.fromSeed(seed, p2pkh.getDefaultDerivation());
Assert.assertEquals("xpub6DCH2YkjweBu5zQheCWgSu6o26AENhApkS2taXaJBsi6vthRytPTaY2Sh4zDHj7oCVhYxx5974HbSbKxh26ah7N6VVw1U8kS2H5HfPUXecq", keystore.getExtendedPublicKey().toString());
Assert.assertEquals("xpub6D9jqMkBdgTqrzTxXVo2w8yZCa7HvzJTybFevJ2StHSxBRhs8dzsVEke9TQ9QjZCKbWZvzbc8iSScBbsCiA11wT28hZmCv3YmjSFEqCLmMn", keystore.getExtendedPublicKey().toString());
}
}

View file

@ -0,0 +1,23 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import org.junit.Test;
public class WalletTest {
@Test
public void encryptTest() throws MnemonicException {
String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor";
DeterministicSeed seed = new DeterministicSeed(words, "pp", 0, DeterministicSeed.Type.BIP39);
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(ScriptType.P2PKH);
Keystore keystore = Keystore.fromSeed(seed, wallet.getScriptType().getDefaultDerivation());
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, wallet.getKeystores(), 1));
wallet.encrypt("pass");
wallet.decrypt("pass");
}
}