mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-26 10:06:45 +00:00
rework deterministicseed, keycrypter
This commit is contained in:
parent
9e5a7d0e8d
commit
312143cb61
17 changed files with 221 additions and 174 deletions
|
@ -1,9 +1,6 @@
|
||||||
package com.sparrowwallet.drongo;
|
package com.sparrowwallet.drongo;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.crypto.AESKeyCrypter;
|
import com.sparrowwallet.drongo.crypto.*;
|
||||||
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;
|
||||||
|
@ -258,8 +255,8 @@ public class Utils {
|
||||||
|
|
||||||
public static byte[] decryptAesCbcPkcs7(byte[] initializationVector, byte[] encryptedBytes, byte[] keyBytes) {
|
public static byte[] decryptAesCbcPkcs7(byte[] initializationVector, byte[] encryptedBytes, byte[] keyBytes) {
|
||||||
KeyCrypter keyCrypter = new AESKeyCrypter();
|
KeyCrypter keyCrypter = new AESKeyCrypter();
|
||||||
EncryptedData data = new EncryptedData(initializationVector, encryptedBytes);
|
EncryptedData data = new EncryptedData(initializationVector, encryptedBytes, null);
|
||||||
return keyCrypter.decrypt(data, new KeyParameter(keyBytes));
|
return keyCrypter.decrypt(data, new Key(keyBytes, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convert to a string path, starting with "M/" */
|
/** Convert to a string path, starting with "M/" */
|
||||||
|
|
|
@ -29,7 +29,7 @@ public class AESKeyCrypter implements KeyCrypter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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");
|
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
|
* @throws KeyCrypterException if bytes could not be decrypted
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public byte[] decrypt(EncryptedData dataToDecrypt, KeyParameter aesKey) throws KeyCrypterException {
|
public byte[] decrypt(EncryptedData dataToDecrypt, Key aesKey) throws KeyCrypterException {
|
||||||
if(dataToDecrypt == null || aesKey == null) {
|
if(dataToDecrypt == null || aesKey == null) {
|
||||||
throw new KeyCrypterException("Data and key to decrypt cannot be null");
|
throw new KeyCrypterException("Data and key to decrypt cannot be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ParametersWithIV keyWithIv = new ParametersWithIV(new KeyParameter(aesKey.getKey()), dataToDecrypt.getInitialisationVector());
|
ParametersWithIV keyWithIv = new ParametersWithIV(new KeyParameter(aesKey.getKeyBytes()), dataToDecrypt.getInitialisationVector());
|
||||||
|
|
||||||
// Decrypt the message.
|
// Decrypt the message.
|
||||||
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
|
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
|
||||||
|
@ -71,7 +71,7 @@ public class AESKeyCrypter implements KeyCrypter {
|
||||||
* Password based encryption using AES - CBC - PKCS7
|
* Password based encryption using AES - CBC - PKCS7
|
||||||
*/
|
*/
|
||||||
@Override
|
@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) {
|
if(plainBytes == null || aesKey == null) {
|
||||||
throw new KeyCrypterException("Data and key to encrypt cannot be null");
|
throw new KeyCrypterException("Data and key to encrypt cannot be null");
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ public class AESKeyCrypter implements KeyCrypter {
|
||||||
secureRandom.nextBytes(iv);
|
secureRandom.nextBytes(iv);
|
||||||
}
|
}
|
||||||
|
|
||||||
ParametersWithIV keyWithIv = new ParametersWithIV(aesKey, iv);
|
ParametersWithIV keyWithIv = new ParametersWithIV(new KeyParameter(aesKey.getKeyBytes()), iv);
|
||||||
|
|
||||||
// Encrypt using AES.
|
// Encrypt using AES.
|
||||||
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
|
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 length1 = cipher.processBytes(plainBytes, 0, plainBytes.length, encryptedBytes, 0);
|
||||||
final int length2 = cipher.doFinal(encryptedBytes, length1);
|
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) {
|
} catch (Exception e) {
|
||||||
throw new KeyCrypterException("Could not encrypt bytes.", e);
|
throw new KeyCrypterException("Could not encrypt bytes.", e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,9 @@ public interface AsymmetricKeyCrypter {
|
||||||
EncryptionType getUnderstoodEncryptionType();
|
EncryptionType getUnderstoodEncryptionType();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a KeyParameter (which typically contains an AES key)
|
* Create a ECKey based on the provided password
|
||||||
* @param 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
|
* @throws KeyCrypterException
|
||||||
*/
|
*/
|
||||||
ECKey deriveECKey(CharSequence password) throws KeyCrypterException;
|
ECKey deriveECKey(CharSequence password) throws KeyCrypterException;
|
||||||
|
|
|
@ -65,13 +65,13 @@ public class ECIESKeyCrypter implements AsymmetricKeyCrypter {
|
||||||
throw new InvalidPasswordException();
|
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
|
@Override
|
||||||
public EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, ECKey key) throws KeyCrypterException {
|
public EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, ECKey key) throws KeyCrypterException {
|
||||||
byte[] encryptedBytes = encryptEcies(key, plainBytes, initializationVector);
|
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) {
|
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_e = Arrays.copyOfRange(hash, 16, 32);
|
||||||
byte[] key_m = Arrays.copyOfRange(hash, 32, 64);
|
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[] encrypted = concat(magic, ephemeral.getPubKey(), ciphertext);
|
||||||
byte[] result = hmac256(key_m, encrypted);
|
byte[] result = hmac256(key_m, encrypted);
|
||||||
return Base64.getEncoder().encode(concat(encrypted, result));
|
return Base64.getEncoder().encode(concat(encrypted, result));
|
||||||
|
|
|
@ -7,14 +7,11 @@ import org.bouncycastle.asn1.*;
|
||||||
import org.bouncycastle.asn1.x9.X9ECParameters;
|
import org.bouncycastle.asn1.x9.X9ECParameters;
|
||||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||||
import org.bouncycastle.crypto.digests.SHA256Digest;
|
import org.bouncycastle.crypto.digests.SHA256Digest;
|
||||||
import org.bouncycastle.crypto.digests.SHA512Digest;
|
|
||||||
import org.bouncycastle.crypto.ec.CustomNamedCurves;
|
import org.bouncycastle.crypto.ec.CustomNamedCurves;
|
||||||
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
|
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
|
||||||
import org.bouncycastle.crypto.macs.HMac;
|
|
||||||
import org.bouncycastle.crypto.params.*;
|
import org.bouncycastle.crypto.params.*;
|
||||||
import org.bouncycastle.crypto.signers.ECDSASigner;
|
import org.bouncycastle.crypto.signers.ECDSASigner;
|
||||||
import org.bouncycastle.crypto.signers.HMacDSAKCalculator;
|
import org.bouncycastle.crypto.signers.HMacDSAKCalculator;
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|
||||||
import org.bouncycastle.math.ec.ECPoint;
|
import org.bouncycastle.math.ec.ECPoint;
|
||||||
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
|
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
|
||||||
import org.bouncycastle.math.ec.FixedPointUtil;
|
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 KeyCrypterException if there's something wrong with aesKey.
|
||||||
* @throws ECKey.MissingPrivateKeyException if this key cannot sign because it's pubkey only.
|
* @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();
|
KeyCrypter crypter = getKeyCrypter();
|
||||||
if (crypter != null) {
|
if (crypter != null) {
|
||||||
if (aesKey == 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.
|
* 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 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
|
* @return encryptedKey
|
||||||
*/
|
*/
|
||||||
public ECKey encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) throws KeyCrypterException {
|
public ECKey encrypt(KeyCrypter keyCrypter, Key aesKey) throws KeyCrypterException {
|
||||||
if(keyCrypter == null) {
|
if(keyCrypter == null) {
|
||||||
throw new KeyCrypterException("Keycrypter cannot be null");
|
throw new KeyCrypterException("Keycrypter cannot be null");
|
||||||
}
|
}
|
||||||
|
@ -748,9 +745,9 @@ public class ECKey implements EncryptableItem {
|
||||||
* just yield a garbage key.
|
* just yield a garbage key.
|
||||||
*
|
*
|
||||||
* @param keyCrypter The keyCrypter that specifies exactly how the decrypted bytes are created.
|
* @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) {
|
if(keyCrypter == null) {
|
||||||
throw new KeyCrypterException("Keycrypter cannot be 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
|
* has some chance of throwing KeyCrypterException due to the corrupted padding that will result, but it can also
|
||||||
* just yield a garbage key.
|
* 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();
|
final KeyCrypter crypter = getKeyCrypter();
|
||||||
if (crypter == null) {
|
if (crypter == null) {
|
||||||
throw new KeyCrypterException("No key crypter available");
|
throw new KeyCrypterException("No key crypter available");
|
||||||
|
@ -794,7 +791,7 @@ public class ECKey implements EncryptableItem {
|
||||||
/**
|
/**
|
||||||
* Creates decrypted private key if needed.
|
* 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;
|
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.
|
* @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 {
|
try {
|
||||||
ECKey rebornUnencryptedKey = encryptedKey.decrypt(keyCrypter, aesKey);
|
ECKey rebornUnencryptedKey = encryptedKey.decrypt(keyCrypter, aesKey);
|
||||||
byte[] originalPrivateKeyBytes = originalKey.getPrivKeyBytes();
|
byte[] originalPrivateKeyBytes = originalKey.getPrivKeyBytes();
|
||||||
|
|
|
@ -13,10 +13,12 @@ import java.util.Objects;
|
||||||
public final class EncryptedData {
|
public final class EncryptedData {
|
||||||
private final byte[] initialisationVector;
|
private final byte[] initialisationVector;
|
||||||
private final byte[] encryptedBytes;
|
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.initialisationVector = Arrays.copyOf(initialisationVector, initialisationVector.length);
|
||||||
this.encryptedBytes = Arrays.copyOf(encryptedBytes, encryptedBytes.length);
|
this.encryptedBytes = Arrays.copyOf(encryptedBytes, encryptedBytes.length);
|
||||||
|
this.keySalt = keySalt == null ? null : Arrays.copyOf(keySalt, keySalt.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getInitialisationVector() {
|
public byte[] getInitialisationVector() {
|
||||||
|
@ -27,27 +29,33 @@ public final class EncryptedData {
|
||||||
return encryptedBytes;
|
return encryptedBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getKeySalt() {
|
||||||
|
return keySalt;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
EncryptedData other = (EncryptedData) o;
|
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
|
@Override
|
||||||
public int hashCode() {
|
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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "EncryptedData [initialisationVector=" + Arrays.toString(initialisationVector)
|
return "EncryptedData [initialisationVector=" + Arrays.toString(initialisationVector)
|
||||||
+ ", encryptedPrivateKey=" + Arrays.toString(encryptedBytes) + "]";
|
+ ", encryptedPrivateKey=" + Arrays.toString(encryptedBytes)
|
||||||
|
+ ", keySalt=" + Arrays.toString(keySalt) + "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
public EncryptedData copy() {
|
public EncryptedData copy() {
|
||||||
return new EncryptedData(Arrays.copyOf(initialisationVector, initialisationVector.length),
|
return new EncryptedData(Arrays.copyOf(initialisationVector, initialisationVector.length),
|
||||||
Arrays.copyOf(encryptedBytes, encryptedBytes.length));
|
Arrays.copyOf(encryptedBytes, encryptedBytes.length),
|
||||||
|
Arrays.copyOf(keySalt, keySalt.length));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
19
src/main/java/com/sparrowwallet/drongo/crypto/Key.java
Normal file
19
src/main/java/com/sparrowwallet/drongo/crypto/Key.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,14 @@
|
||||||
package com.sparrowwallet.drongo.crypto;
|
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
|
* <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>
|
* 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>
|
* 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>
|
* 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
|
* <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>
|
* 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();
|
EncryptionType getUnderstoodEncryptionType();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a KeyParameter (which typically contains an AES key)
|
* Create a Key (which typically contains an AES key)
|
||||||
* @param password
|
* @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
|
* @throws KeyCrypterException
|
||||||
*/
|
*/
|
||||||
KeyParameter deriveKey(CharSequence password) throws KeyCrypterException;
|
Key deriveKey(CharSequence password) throws KeyCrypterException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypt the provided encrypted bytes, converting them into unencrypted bytes.
|
* Decrypt the provided encrypted bytes, converting them into unencrypted bytes.
|
||||||
*
|
*
|
||||||
* @throws KeyCrypterException if decryption was unsuccessful.
|
* @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.
|
* 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.
|
* @return encryptedPrivateKey An encryptedPrivateKey containing the encrypted bytes and an initialisation vector.
|
||||||
* @throws KeyCrypterException if encryption was unsuccessful
|
* @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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.sparrowwallet.drongo.crypto;
|
package com.sparrowwallet.drongo.crypto;
|
||||||
|
|
||||||
import org.bouncycastle.crypto.generators.SCrypt;
|
import org.bouncycastle.crypto.generators.SCrypt;
|
||||||
import org.bouncycastle.crypto.params.KeyParameter;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -54,6 +53,13 @@ public class ScryptKeyCrypter extends AESKeyCrypter {
|
||||||
this.scryptParameters = new ScryptParameters(randomSalt());
|
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.
|
* 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).
|
* 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.
|
* 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
|
* @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
|
* @throws KeyCrypterException
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public KeyParameter deriveKey(CharSequence password) throws KeyCrypterException {
|
public Key deriveKey(CharSequence password) throws KeyCrypterException {
|
||||||
byte[] passwordBytes = null;
|
byte[] passwordBytes = null;
|
||||||
try {
|
try {
|
||||||
passwordBytes = convertToByteArray(password);
|
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);
|
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) {
|
} catch (Exception e) {
|
||||||
throw new KeyCrypterException("Could not generate key from password and salt.", e);
|
throw new KeyCrypterException("Could not generate key from password and salt.", e);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -95,7 +95,7 @@ public class Bip39MnemonicCode {
|
||||||
*/
|
*/
|
||||||
public static byte[] toSeed(List<String> words, String passphrase) {
|
public static byte[] toSeed(List<String> words, String passphrase) {
|
||||||
if(passphrase == null) {
|
if(passphrase == null) {
|
||||||
throw new IllegalArgumentException("A null passphrase is not allowed.");
|
passphrase = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// To create binary seed from mnemonic, we use PBKDF2 function
|
// To create binary seed from mnemonic, we use PBKDF2 function
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
package com.sparrowwallet.drongo.wallet;
|
package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.crypto.EncryptableItem;
|
import com.sparrowwallet.drongo.crypto.*;
|
||||||
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 java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
@ -16,52 +12,39 @@ public class DeterministicSeed implements EncryptableItem {
|
||||||
public static final int MAX_SEED_ENTROPY_BITS = 512;
|
public static final int MAX_SEED_ENTROPY_BITS = 512;
|
||||||
|
|
||||||
private final Type type;
|
private final Type type;
|
||||||
|
|
||||||
private final byte[] seed;
|
|
||||||
private final List<String> mnemonicCode;
|
private final List<String> mnemonicCode;
|
||||||
|
|
||||||
private final EncryptedData encryptedSeed;
|
|
||||||
private final EncryptedData encryptedMnemonicCode;
|
private final EncryptedData encryptedMnemonicCode;
|
||||||
|
private final boolean needsPassphrase;
|
||||||
private long creationTimeSeconds;
|
private long creationTimeSeconds;
|
||||||
|
|
||||||
//Session only storage
|
//Session only storage
|
||||||
private transient String passphrase;
|
private transient String passphrase;
|
||||||
|
|
||||||
public DeterministicSeed(String mnemonicString, byte[] seed, String passphrase, long creationTimeSeconds, Type type) {
|
public DeterministicSeed(String mnemonicString, String passphrase, long creationTimeSeconds, Type type) {
|
||||||
this(decodeMnemonicCode(mnemonicString), seed, passphrase, creationTimeSeconds, type);
|
this(decodeMnemonicCode(mnemonicString), passphrase, creationTimeSeconds, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DeterministicSeed(byte[] seed, List<String> mnemonic, long creationTimeSeconds, Type type) {
|
public DeterministicSeed(List<String> mnemonic, String passphrase, long creationTimeSeconds, Type type) {
|
||||||
this.seed = seed;
|
this(mnemonic, needsPassphrase(passphrase), creationTimeSeconds, type);
|
||||||
this.encryptedSeed = null;
|
this.passphrase = passphrase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeterministicSeed(List<String> mnemonic, boolean needsPassphrase, long creationTimeSeconds, Type type) {
|
||||||
this.mnemonicCode = mnemonic;
|
this.mnemonicCode = mnemonic;
|
||||||
this.encryptedMnemonicCode = null;
|
this.encryptedMnemonicCode = null;
|
||||||
|
this.needsPassphrase = needsPassphrase;
|
||||||
this.creationTimeSeconds = creationTimeSeconds;
|
this.creationTimeSeconds = creationTimeSeconds;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DeterministicSeed(EncryptedData encryptedMnemonic, EncryptedData encryptedSeed, long creationTimeSeconds, Type type) {
|
public DeterministicSeed(EncryptedData encryptedMnemonic, boolean needsPassphrase, long creationTimeSeconds, Type type) {
|
||||||
this.seed = null;
|
|
||||||
this.encryptedSeed = encryptedSeed;
|
|
||||||
this.mnemonicCode = null;
|
this.mnemonicCode = null;
|
||||||
this.encryptedMnemonicCode = encryptedMnemonic;
|
this.encryptedMnemonicCode = encryptedMnemonic;
|
||||||
|
this.needsPassphrase = needsPassphrase;
|
||||||
this.creationTimeSeconds = creationTimeSeconds;
|
this.creationTimeSeconds = creationTimeSeconds;
|
||||||
this.type = type;
|
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
|
* Constructs a new BIP39 seed. See {@link Bip39MnemonicCode} for more
|
||||||
* details on this scheme.
|
* details on this scheme.
|
||||||
|
@ -99,20 +82,26 @@ public class DeterministicSeed implements EncryptableItem {
|
||||||
// cannot happen
|
// cannot happen
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
this.seed = Bip39MnemonicCode.toSeed(mnemonicCode, passphrase);
|
|
||||||
this.encryptedMnemonicCode = null;
|
this.encryptedMnemonicCode = null;
|
||||||
this.encryptedSeed = null;
|
this.needsPassphrase = needsPassphrase(passphrase);
|
||||||
this.creationTimeSeconds = creationTimeSeconds;
|
this.creationTimeSeconds = creationTimeSeconds;
|
||||||
this.type = Type.BIP39;
|
this.type = Type.BIP39;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean usesPassphrase() {
|
public static boolean needsPassphrase(String passphrase) {
|
||||||
if(isEncrypted()) {
|
return passphrase != null && !passphrase.isEmpty();
|
||||||
throw new IllegalArgumentException("Cannot determine if passphrase is required in encrypted state");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
byte[] mnemonicOnlySeed = type.toSeed(mnemonicCode, "");
|
public boolean needsPassphrase() {
|
||||||
return Arrays.equals(mnemonicOnlySeed, seed);
|
return needsPassphrase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassphrase() {
|
||||||
|
return passphrase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassphrase(String passphrase) {
|
||||||
|
this.passphrase = passphrase;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] getEntropy(SecureRandom random, int bits) {
|
private static byte[] getEntropy(SecureRandom random, int bits) {
|
||||||
|
@ -137,14 +126,15 @@ public class DeterministicSeed implements EncryptableItem {
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
if(isEncrypted()) {
|
if(isEncrypted()) {
|
||||||
return encryptedSeed.toString();
|
return encryptedMnemonicCode.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return toHexString();
|
return getMnemonicString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the seed as hex or null if encrypted. */
|
/** 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;
|
return seed != null ? Utils.bytesToHex(seed) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,8 +143,12 @@ public class DeterministicSeed implements EncryptableItem {
|
||||||
return getMnemonicAsBytes();
|
return getMnemonicAsBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getSeedBytes() {
|
public byte[] getSeedBytes() throws MnemonicException {
|
||||||
return seed;
|
if(passphrase == null && needsPassphrase) {
|
||||||
|
throw new MnemonicException("Passphrase required but not provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
return type.toSeed(mnemonicCode, passphrase);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -167,10 +161,6 @@ public class DeterministicSeed implements EncryptableItem {
|
||||||
return EncryptionType.ENCRYPTED_SCRYPT_AES;
|
return EncryptionType.ENCRYPTED_SCRYPT_AES;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EncryptedData getEncryptedSeedData() {
|
|
||||||
return encryptedSeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getCreationTimeSeconds() {
|
public long getCreationTimeSeconds() {
|
||||||
return creationTimeSeconds;
|
return creationTimeSeconds;
|
||||||
|
@ -184,37 +174,27 @@ public class DeterministicSeed implements EncryptableItem {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DeterministicSeed encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) {
|
public DeterministicSeed encrypt(KeyCrypter keyCrypter, Key aesKey) {
|
||||||
if(encryptedSeed != null) {
|
if(encryptedMnemonicCode != null) {
|
||||||
throw new IllegalArgumentException("Trying to encrypt seed twice");
|
throw new IllegalArgumentException("Trying to encrypt twice");
|
||||||
}
|
}
|
||||||
if(mnemonicCode == null) {
|
if(mnemonicCode == null) {
|
||||||
throw new IllegalArgumentException("Mnemonic missing so cannot encrypt");
|
throw new IllegalArgumentException("Mnemonic missing so cannot encrypt");
|
||||||
}
|
}
|
||||||
EncryptedData encryptedMnemonic = keyCrypter.encrypt(getMnemonicAsBytes(), null, aesKey);
|
EncryptedData encryptedMnemonic = keyCrypter.encrypt(getMnemonicAsBytes(), null, aesKey);
|
||||||
EncryptedData encryptedSeed = keyCrypter.encrypt(seed, null, aesKey);
|
return new DeterministicSeed(encryptedMnemonic, needsPassphrase, creationTimeSeconds, type);
|
||||||
return new DeterministicSeed(encryptedMnemonic, encryptedSeed, creationTimeSeconds, type);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] getMnemonicAsBytes() {
|
private byte[] getMnemonicAsBytes() {
|
||||||
return getMnemonicString().getBytes(StandardCharsets.UTF_8);
|
return getMnemonicString().getBytes(StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DeterministicSeed decrypt(KeyCrypter crypter, String passphrase, KeyParameter aesKey) {
|
public DeterministicSeed decrypt(KeyCrypter crypter, Key aesKey) {
|
||||||
if(!isEncrypted()) {
|
if(!isEncrypted()) {
|
||||||
throw new IllegalStateException("Cannot decrypt unencrypted seed");
|
throw new IllegalStateException("Cannot decrypt unencrypted seed");
|
||||||
}
|
}
|
||||||
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);
|
return new DeterministicSeed(mnemonic, needsPassphrase, creationTimeSeconds, type);
|
||||||
return new DeterministicSeed(mnemonic, seed, passphrase, creationTimeSeconds, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPassphrase() {
|
|
||||||
return passphrase;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPassphrase(String passphrase) {
|
|
||||||
this.passphrase = passphrase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -276,11 +256,16 @@ public class DeterministicSeed implements EncryptableItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DeterministicSeed copy() {
|
public DeterministicSeed copy() {
|
||||||
|
DeterministicSeed seed;
|
||||||
|
|
||||||
if(isEncrypted()) {
|
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 {
|
public enum Type {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import com.sparrowwallet.drongo.ExtendedKey;
|
||||||
import com.sparrowwallet.drongo.KeyDerivation;
|
import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.crypto.*;
|
import com.sparrowwallet.drongo.crypto.*;
|
||||||
import org.bouncycastle.crypto.params.KeyParameter;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -78,7 +77,7 @@ public class Keystore {
|
||||||
this.seed = seed;
|
this.seed = seed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DeterministicKey getMasterPrivateKey() {
|
public DeterministicKey getMasterPrivateKey() throws MnemonicException {
|
||||||
if(seed == null) {
|
if(seed == null) {
|
||||||
throw new IllegalArgumentException("Keystore does not contain a seed");
|
throw new IllegalArgumentException("Keystore does not contain a seed");
|
||||||
}
|
}
|
||||||
|
@ -90,15 +89,15 @@ public class Keystore {
|
||||||
return HDKeyDerivation.createMasterPrivateKey(seed.getSeedBytes());
|
return HDKeyDerivation.createMasterPrivateKey(seed.getSeedBytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExtendedKey getExtendedMasterPrivateKey() {
|
public ExtendedKey getExtendedMasterPrivateKey() throws MnemonicException {
|
||||||
return new ExtendedKey(getMasterPrivateKey(), new byte[4], ChildNumber.ZERO);
|
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);
|
return new ExtendedKey(getMasterPrivateKey().dropPrivateBytes(), new byte[4], ChildNumber.ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExtendedKey getExtendedPrivateKey() {
|
public ExtendedKey getExtendedPrivateKey() throws MnemonicException {
|
||||||
List<ChildNumber> derivation = getKeyDerivation().getDerivation();
|
List<ChildNumber> derivation = getKeyDerivation().getDerivation();
|
||||||
DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation);
|
DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation);
|
||||||
return new ExtendedKey(derivedKey, derivedKey.getParentFingerprint(), derivation.get(derivation.size() - 1));
|
return new ExtendedKey(derivedKey, derivedKey.getParentFingerprint(), derivation.get(derivation.size() - 1));
|
||||||
|
@ -126,12 +125,18 @@ public class Keystore {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ChildNumber> derivation = getKeyDerivation().getDerivation();
|
if(!seed.isEncrypted()) {
|
||||||
DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation);
|
try {
|
||||||
DeterministicKey derivedKeyPublicOnly = derivedKey.dropPrivateBytes().dropParent();
|
List<ChildNumber> derivation = getKeyDerivation().getDerivation();
|
||||||
ExtendedKey xpub = new ExtendedKey(derivedKeyPublicOnly, derivedKey.getParentFingerprint(), derivation.get(derivation.size() - 1));
|
DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation);
|
||||||
if(!xpub.equals(getExtendedPublicKey())) {
|
DeterministicKey derivedKeyPublicOnly = derivedKey.dropPrivateBytes().dropParent();
|
||||||
return false;
|
ExtendedKey xpub = new ExtendedKey(derivedKeyPublicOnly, derivedKey.getParentFingerprint(), derivation.get(derivation.size() - 1));
|
||||||
|
if(!xpub.equals(getExtendedPublicKey())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch(MnemonicException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,7 +159,7 @@ public class Keystore {
|
||||||
return copy;
|
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 keystore = new Keystore();
|
||||||
keystore.setSeed(seed);
|
keystore.setSeed(seed);
|
||||||
ExtendedKey xprv = keystore.getExtendedMasterPrivateKey();
|
ExtendedKey xprv = keystore.getExtendedMasterPrivateKey();
|
||||||
|
@ -181,24 +186,16 @@ public class Keystore {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void encrypt(String password) {
|
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()) {
|
if(seed != null && !seed.isEncrypted()) {
|
||||||
seed = seed.encrypt(keyCrypter, key);
|
KeyCrypter keyCrypter = new ScryptKeyCrypter();
|
||||||
|
seed = seed.encrypt(keyCrypter, keyCrypter.deriveKey(password));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void decrypt(String password, String passphrase) {
|
public void decrypt(String password) {
|
||||||
KeyCrypter keyCrypter = new ScryptKeyCrypter();
|
|
||||||
decrypt(keyCrypter, passphrase, keyCrypter.deriveKey(password));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void decrypt(KeyCrypter keyCrypter, String passphrase, KeyParameter key) {
|
|
||||||
if(seed != null && seed.isEncrypted()) {
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
package com.sparrowwallet.drongo.wallet;
|
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.Policy;
|
||||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
import org.bouncycastle.crypto.params.KeyParameter;
|
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
@ -139,6 +136,16 @@ public class Wallet {
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean containsSeeds() {
|
||||||
|
for(Keystore keystore : keystores) {
|
||||||
|
if(keystore.hasSeed()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isEncrypted() {
|
public boolean isEncrypted() {
|
||||||
for(Keystore keystore : keystores) {
|
for(Keystore keystore : keystores) {
|
||||||
if(keystore.isEncrypted()) {
|
if(keystore.isEncrypted()) {
|
||||||
|
@ -150,18 +157,14 @@ public class Wallet {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void encrypt(String password) {
|
public void encrypt(String password) {
|
||||||
KeyCrypter keyCrypter = new ScryptKeyCrypter();
|
|
||||||
KeyParameter key = keyCrypter.deriveKey(password);
|
|
||||||
for(Keystore keystore : keystores) {
|
for(Keystore keystore : keystores) {
|
||||||
keystore.encrypt(keyCrypter, key);
|
keystore.encrypt(password);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void decrypt(String password, String passphrase) {
|
public void decrypt(String password) {
|
||||||
KeyCrypter keyCrypter = new ScryptKeyCrypter();
|
|
||||||
KeyParameter key = keyCrypter.deriveKey(password);
|
|
||||||
for(Keystore keystore : keystores) {
|
for(Keystore keystore : keystores) {
|
||||||
keystore.decrypt(keyCrypter, passphrase, key);
|
keystore.decrypt(password);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,33 @@
|
||||||
package com.sparrowwallet.drongo.crypto;
|
package com.sparrowwallet.drongo.crypto;
|
||||||
|
|
||||||
import org.bouncycastle.crypto.params.KeyParameter;
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.security.Security;
|
|
||||||
|
|
||||||
public class ScryptKeyCrypterTest {
|
public class ScryptKeyCrypterTest {
|
||||||
@Test
|
@Test
|
||||||
public void testScrypt() {
|
public void testScrypt() {
|
||||||
ScryptKeyCrypter scryptKeyCrypter = new ScryptKeyCrypter();
|
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[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
byte[] iv = new byte[16];
|
byte[] iv = new byte[16];
|
||||||
SecureRandom secureRandom = new SecureRandom();
|
SecureRandom secureRandom = new SecureRandom();
|
||||||
secureRandom.nextBytes(iv);
|
secureRandom.nextBytes(iv);
|
||||||
|
|
||||||
EncryptedData scrypted = scryptKeyCrypter.encrypt(messageBytes, iv, keyParameter);
|
EncryptedData scrypted = scryptKeyCrypter.encrypt(messageBytes, iv, key);
|
||||||
|
|
||||||
AESKeyCrypter aesKeyCrypter = new AESKeyCrypter();
|
AESKeyCrypter aesKeyCrypter = new AESKeyCrypter();
|
||||||
EncryptedData aescrypted = aesKeyCrypter.encrypt(messageBytes, iv, keyParameter);
|
EncryptedData aescrypted = aesKeyCrypter.encrypt(messageBytes, iv, key);
|
||||||
|
|
||||||
Assert.assertArrayEquals(scrypted.getEncryptedBytes(), aescrypted.getEncryptedBytes());
|
Assert.assertArrayEquals(scrypted.getEncryptedBytes(), aescrypted.getEncryptedBytes());
|
||||||
|
|
||||||
byte[] sdecrypted = scryptKeyCrypter.decrypt(scrypted, keyParameter);
|
byte[] sdecrypted = scryptKeyCrypter.decrypt(scrypted, key);
|
||||||
byte[] aesdecrypted = aesKeyCrypter.decrypt(aescrypted, keyParameter);
|
byte[] aesdecrypted = aesKeyCrypter.decrypt(aescrypted, key);
|
||||||
|
|
||||||
Assert.assertArrayEquals(sdecrypted, aesdecrypted);
|
Assert.assertArrayEquals(sdecrypted, aesdecrypted);
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,29 +9,20 @@ import java.util.Collections;
|
||||||
|
|
||||||
public class KeystoreTest {
|
public class KeystoreTest {
|
||||||
@Test
|
@Test
|
||||||
public void testExtendedPrivateKey() {
|
public void testExtendedPrivateKey() throws MnemonicException {
|
||||||
Keystore keystore = new Keystore();
|
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);
|
keystore.setSeed(seed);
|
||||||
|
|
||||||
Assert.assertEquals("xprv9s21ZrQH143K3rN5vhm4bKDKsk1PmUK1mzxSMwkVSp2GbomwGmjLaGqrs8Nn9r14jCsfCNWfTR6pAtCsJutUH6QSHX65CePNW3YVyGxqvJa", keystore.getExtendedMasterPrivateKey().toString());
|
Assert.assertEquals("xprv9s21ZrQH143K3rN5vhm4bKDKsk1PmUK1mzxSMwkVSp2GbomwGmjLaGqrs8Nn9r14jCsfCNWfTR6pAtCsJutUH6QSHX65CePNW3YVyGxqvJa", keystore.getExtendedMasterPrivateKey().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testExtendedPrivateKeyTwo() {
|
public void testFromSeed() throws MnemonicException {
|
||||||
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() {
|
|
||||||
ScriptType p2pkh = ScriptType.P2PKH;
|
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());
|
Keystore keystore = Keystore.fromSeed(seed, p2pkh.getDefaultDerivation());
|
||||||
|
|
||||||
Assert.assertEquals("xpub6DCH2YkjweBu5zQheCWgSu6o26AENhApkS2taXaJBsi6vthRytPTaY2Sh4zDHj7oCVhYxx5974HbSbKxh26ah7N6VVw1U8kS2H5HfPUXecq", keystore.getExtendedPublicKey().toString());
|
Assert.assertEquals("xpub6D9jqMkBdgTqrzTxXVo2w8yZCa7HvzJTybFevJ2StHSxBRhs8dzsVEke9TQ9QjZCKbWZvzbc8iSScBbsCiA11wT28hZmCv3YmjSFEqCLmMn", keystore.getExtendedPublicKey().toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue