refactor keycrypter hierarchy, add ECIESKeyCrypter

This commit is contained in:
Craig Raw 2020-05-11 17:39:23 +02:00
parent c675e395db
commit 492f447c28
14 changed files with 337 additions and 258 deletions

View file

@ -1,67 +1,101 @@
package com.sparrowwallet.drongo.crypto; package com.sparrowwallet.drongo.crypto;
import com.sparrowwallet.drongo.Utils; import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import javax.crypto.Cipher; import java.security.SecureRandom;
import javax.crypto.spec.IvParameterSpec; import java.util.Arrays;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.spec.AlgorithmParameterSpec;
/*
*
*/
public class AESKeyCrypter implements KeyCrypter { public class AESKeyCrypter implements KeyCrypter {
/**
* The size of an AES block in bytes.
* This is also the length of the initialisation vector.
*/
public static final int BLOCK_LENGTH = 16; // = 128 bits.
private static final SecureRandom secureRandom = new SecureRandom();
@Override @Override
public EncryptionType getUnderstoodEncryptionType() { public EncryptionType getUnderstoodEncryptionType() {
return null; return EncryptionType.ENCRYPTED_AES;
} }
@Override @Override
public KeyParameter deriveKey(CharSequence password) throws KeyCrypterException { public KeyParameter deriveKey(CharSequence password) throws KeyCrypterException {
return createKeyPbkdf2HmacSha512(password.toString()); throw new UnsupportedOperationException("AESKeyCrypter does not define a key derivation function, but keys must be either 128, 192 or 256 bits long");
}
public static KeyParameter createKeyPbkdf2HmacSha512(String password) {
return createKeyPbkdf2HmacSha512(password, new byte[0], 1024);
}
public static KeyParameter createKeyPbkdf2HmacSha512(String password, byte[] salt, int iterationCount) {
byte[] secret = Utils.getPbkdf2HmacSha512Hash(password.getBytes(StandardCharsets.UTF_8), salt, iterationCount);
return new KeyParameter(secret);
} }
/**
* Decrypt bytes previously encrypted with this class.
*
* @param dataToDecrypt The data to decrypt
* @param aesKey The AES key to use for decryption
* @return The decrypted bytes
* @throws KeyCrypterException if bytes could not be decrypted
*/
@Override @Override
public byte[] decrypt(EncryptedData encryptedBytesToDecode, KeyParameter aesKey) throws KeyCrypterException { public byte[] decrypt(EncryptedData dataToDecrypt, KeyParameter aesKey) throws KeyCrypterException {
return decryptAesCbcPkcs7(encryptedBytesToDecode.getEncryptedBytes(), encryptedBytesToDecode.getInitialisationVector(), aesKey.getKey()); if(dataToDecrypt == null || aesKey == null) {
} throw new KeyCrypterException("Data and key to decrypt cannot be null");
}
private byte[] decryptAesCbcPkcs7(byte[] ciphertext, byte[] iv, byte[] key_e) {
try { try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); ParametersWithIV keyWithIv = new ParametersWithIV(new KeyParameter(aesKey.getKey()), dataToDecrypt.getInitialisationVector());
SecretKeySpec secretKeySpec = new SecretKeySpec(key_e, "AES");
AlgorithmParameterSpec paramSpec = new IvParameterSpec(iv); // Decrypt the message.
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, paramSpec); BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
return cipher.doFinal(ciphertext); cipher.init(false, keyWithIv);
} catch (Exception e) {
throw new KeyCrypterException("Error decrypting", e); byte[] cipherBytes = dataToDecrypt.getEncryptedBytes();
byte[] decryptedBytes = new byte[cipher.getOutputSize(cipherBytes.length)];
final int length1 = cipher.processBytes(cipherBytes, 0, cipherBytes.length, decryptedBytes, 0);
final int length2 = cipher.doFinal(decryptedBytes, length1);
return Arrays.copyOf(decryptedBytes, length1 + length2);
} catch (InvalidCipherTextException e) {
throw new KeyCrypterException.InvalidCipherText("Could not decrypt bytes", e);
} catch (RuntimeException e) {
throw new KeyCrypterException("Could not decrypt bytes", e);
} }
} }
/**
* Password based encryption using AES - CBC 256 bits.
*/
@Override @Override
public EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, KeyParameter aesKey) throws KeyCrypterException { public EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, KeyParameter aesKey) throws KeyCrypterException {
byte[] encryptedData = encryptAesCbcPkcs7(plainBytes, initializationVector, aesKey.getKey()); if(plainBytes == null || aesKey == null) {
return new EncryptedData(initializationVector, encryptedData); throw new KeyCrypterException("Data and key to encrypt cannot be null");
} }
private byte[] encryptAesCbcPkcs7(byte[] message, byte[] iv, byte[] key_e) {
try { try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); // Generate iv - each encryption call has a different iv.
SecretKeySpec secretKeySpec = new SecretKeySpec(key_e, "AES"); byte[] iv = initializationVector;
AlgorithmParameterSpec paramSpec = new IvParameterSpec(iv); if(iv == null) {
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, paramSpec); iv = new byte[BLOCK_LENGTH];
return cipher.doFinal(message); secureRandom.nextBytes(iv);
} catch(Exception e) { }
throw new KeyCrypterException("Could not encrypt", e);
ParametersWithIV keyWithIv = new ParametersWithIV(aesKey, iv);
// Encrypt using AES.
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
cipher.init(true, keyWithIv);
byte[] encryptedBytes = new byte[cipher.getOutputSize(plainBytes.length)];
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));
} catch (Exception e) {
throw new KeyCrypterException("Could not encrypt bytes.", e);
} }
} }
} }

View file

@ -0,0 +1,32 @@
package com.sparrowwallet.drongo.crypto;
public interface AsymmetricKeyCrypter {
/**
* Return the EncryptionType enum value which denotes the type of encryption/ decryption that this KeyCrypter
* can understand.
*/
EncryptionType getUnderstoodEncryptionType();
/**
* Create a KeyParameter (which typically contains an AES key)
* @param password
* @return KeyParameter The KeyParameter which typically contains the AES key to use for encrypting and decrypting
* @throws KeyCrypterException
*/
ECKey deriveECKey(CharSequence password) throws KeyCrypterException;
/**
* Decrypt the provided encrypted bytes, converting them into unencrypted bytes.
*
* @throws KeyCrypterException if decryption was unsuccessful.
*/
byte[] decrypt(EncryptedData encryptedBytesToDecode, ECKey key) throws KeyCrypterException;
/**
* Encrypt the supplied bytes, converting them into ciphertext.
*
* @return encryptedPrivateKey An encryptedPrivateKey containing the encrypted bytes and an initialisation vector.
* @throws KeyCrypterException if encryption was unsuccessful
*/
EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, ECKey key) throws KeyCrypterException;
}

View file

@ -0,0 +1,128 @@
package com.sparrowwallet.drongo.crypto;
import com.sparrowwallet.drongo.Utils;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
/*
* Encrypt data according to Electrum ECIES format (also called BIE1)
*/
public class ECIESKeyCrypter implements AsymmetricKeyCrypter {
private final KeyCrypter aesKeyCrypter = new AESKeyCrypter();
@Override
public EncryptionType getUnderstoodEncryptionType() {
return EncryptionType.ENCRYPTED_ECIES_AES;
}
@Override
public ECKey deriveECKey(CharSequence password) throws KeyCrypterException {
return deriveECKey(password.toString());
}
public static ECKey deriveECKey(String password) throws KeyCrypterException {
byte[] secret = Utils.getPbkdf2HmacSha512Hash(password.toString().getBytes(StandardCharsets.UTF_8), new byte[0], 1024);
return ECKey.fromPrivate(secret);
}
@Override
public byte[] decrypt(EncryptedData encryptedBytesToDecode, ECKey key) throws KeyCrypterException {
return decryptEcies(encryptedBytesToDecode.getEncryptedBytes(), encryptedBytesToDecode.getInitialisationVector(), key);
}
public byte[] decryptEcies(byte[] message, byte[] magic, ECKey key) {
byte[] decoded = Base64.getDecoder().decode(message);
if(decoded.length < 85) {
throw new IllegalArgumentException("Ciphertext is too short at " + decoded.length + " bytes");
}
byte[] magicFound = Arrays.copyOfRange(decoded, 0, 4); //new byte[4];
//System.arraycopy(decoded, 0, magicFound, 0, 4);
byte[] ephemeralPubKeyBytes = Arrays.copyOfRange(decoded, 4, 37); //new byte[33];
//System.arraycopy(decoded, 4, ephemeralPubKeyBytes, 0, 33);
int ciphertextlength = decoded.length - 37 - 32;
byte[] ciphertext = Arrays.copyOfRange(decoded, 37, decoded.length - 32); //new byte[ciphertextlength];
//System.arraycopy(decoded, 37, ciphertext, 0, ciphertextlength);
byte[] mac = Arrays.copyOfRange(decoded, decoded.length - 32, decoded.length); //new byte[32];
//System.arraycopy(decoded, decoded.length - 32, mac, 0, 32);
if(!Arrays.equals(magic, magicFound)) {
throw new IllegalArgumentException("Invalid ciphertext: invalid magic bytes");
}
ECKey ephemeralPubKey = ECKey.fromPublicOnly(ephemeralPubKeyBytes);
byte[] ecdh_key = ephemeralPubKey.getPubKeyPoint().multiply(key.getPrivKey()).getEncoded(true);
byte[] hash = sha512(ecdh_key);
byte[] iv = Arrays.copyOfRange(hash, 0, 16);
byte[] key_e = Arrays.copyOfRange(hash, 16, 32);
byte[] key_m = Arrays.copyOfRange(hash, 32, 64);
byte[] hmacInput = Arrays.copyOfRange(decoded, 0, decoded.length - 32);
if(!Arrays.equals(mac, hmac256(key_m, hmacInput))) {
throw new InvalidPasswordException();
}
return aesKeyCrypter.decrypt(new EncryptedData(iv, ciphertext), new KeyParameter(key_e));
}
@Override
public EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, ECKey key) throws KeyCrypterException {
byte[] encryptedBytes = encryptEcies(key, plainBytes, initializationVector);
return new EncryptedData(initializationVector, encryptedBytes);
}
public byte[] encryptEcies(ECKey key, byte[] message, byte[] magic) {
ECKey ephemeral = new ECKey();
byte[] ecdh_key = key.getPubKeyPoint().multiply(ephemeral.getPrivKey()).getEncoded(true);
byte[] hash = sha512(ecdh_key);
byte[] iv = Arrays.copyOfRange(hash, 0, 16);
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[] encrypted = concat(magic, ephemeral.getPubKey(), ciphertext);
byte[] result = hmac256(key_m, encrypted);
return Base64.getEncoder().encode(concat(encrypted, result));
}
private byte[] sha512(byte[] input) {
SHA512Digest digest = new SHA512Digest();
byte[] hash = new byte[digest.getDigestSize()];
digest.update(input, 0, input.length);
digest.doFinal(hash, 0);
return hash;
}
private byte[] hmac256(byte[] key, byte[] input) {
HMac hmac = new HMac(new SHA256Digest());
hmac.init(new KeyParameter(key));
byte[] result = new byte[hmac.getMacSize()];
hmac.update(input, 0, input.length);
hmac.doFinal(result, 0);
return result;
}
private byte[] concat(byte[] ...bytes) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
for(byte[] byteArray : bytes) {
out.write(byteArray);
}
} catch (IOException e) {
//can't happen
}
return out.toByteArray();
}
public static class InvalidPasswordException extends RuntimeException {
}
}

View file

@ -97,8 +97,6 @@ public class ECKey implements EncryptableItem {
CURVE_PARAMS.getH()); CURVE_PARAMS.getH());
HALF_CURVE_ORDER = CURVE_PARAMS.getN().shiftRight(1); HALF_CURVE_ORDER = CURVE_PARAMS.getN().shiftRight(1);
secureRandom = new SecureRandom(); secureRandom = new SecureRandom();
Security.addProvider(new BouncyCastleProvider());
} }
// The two parts of the key. If "pub" is set but not "priv", we can only verify signatures, not make them. // The two parts of the key. If "pub" is set but not "priv", we can only verify signatures, not make them.
@ -879,106 +877,6 @@ public class ECKey implements EncryptableItem {
public static class KeyIsEncryptedException extends MissingPrivateKeyException { public static class KeyIsEncryptedException extends MissingPrivateKeyException {
} }
public static class InvalidPasswordException extends RuntimeException {
}
public static ECKey createKeyPbkdf2HmacSha512(String password) {
return createKeyPbkdf2HmacSha512(password, new byte[0], 1024);
}
public static ECKey createKeyPbkdf2HmacSha512(String password, byte[] salt, int iterationCount) {
byte[] secret = Utils.getPbkdf2HmacSha512Hash(password.getBytes(StandardCharsets.UTF_8), salt, iterationCount);
return ECKey.fromPrivate(secret);
}
public byte[] encryptEcies(byte[] message, byte[] magic) {
ECKey ephemeral = new ECKey();
byte[] ecdh_key = this.getPubKeyPoint().multiply(ephemeral.getPrivKey()).getEncoded(true);
byte[] hash = sha512(ecdh_key);
byte[] iv = new byte[16];
System.arraycopy(hash, 0, iv, 0, 16);
byte[] key_e = new byte[16];
System.arraycopy(hash, 16, key_e, 0, 16);
byte[] key_m = new byte[hash.length-32];
System.arraycopy(hash, 32, key_m, 0, hash.length-32);
AESKeyCrypter aesKeyCrypter = new AESKeyCrypter();
byte[] ciphertext = aesKeyCrypter.encrypt(message, iv, new KeyParameter(key_e)).getEncryptedBytes();
byte[] encrypted = concat(magic, ephemeral.getPubKey(), ciphertext);
byte[] result = hmac256(key_m, encrypted);
return Base64.getEncoder().encode(concat(encrypted, result));
}
public byte[] decryptEcies(byte[] message, byte[] magic) {
byte[] decoded = Base64.getDecoder().decode(message);
if(decoded.length < 85) {
throw new IllegalArgumentException("Ciphertext is too short at " + decoded.length + " bytes");
}
byte[] magicFound = new byte[4];
System.arraycopy(decoded, 0, magicFound, 0, 4);
byte[] ephemeralPubKeyBytes = new byte[33];
System.arraycopy(decoded, 4, ephemeralPubKeyBytes, 0, 33);
int ciphertextlength = decoded.length - 37 - 32;
byte[] ciphertext = new byte[ciphertextlength];
System.arraycopy(decoded, 37, ciphertext, 0, ciphertextlength);
byte[] mac = new byte[32];
System.arraycopy(decoded, decoded.length - 32, mac, 0, 32);
if(!Arrays.equals(magic, magicFound)) {
throw new IllegalArgumentException("Invalid ciphertext: invalid magic bytes");
}
ECKey ephemeralPubKey = ECKey.fromPublicOnly(ephemeralPubKeyBytes);
byte[] ecdh_key = ephemeralPubKey.getPubKeyPoint().multiply(this.getPrivKey()).getEncoded(true);
byte[] hash = sha512(ecdh_key);
byte[] iv = new byte[16];
System.arraycopy(hash, 0, iv, 0, 16);
byte[] key_e = new byte[16];
System.arraycopy(hash, 16, key_e, 0, 16);
byte[] key_m = new byte[hash.length-32];
System.arraycopy(hash, 32, key_m, 0, hash.length-32);
byte[] hmacInput = new byte[decoded.length-32];
System.arraycopy(decoded, 0, hmacInput, 0, decoded.length - 32);
if(!Arrays.equals(mac, hmac256(key_m, hmacInput))) {
throw new InvalidPasswordException();
}
AESKeyCrypter aesKeyCrypter = new AESKeyCrypter();
return aesKeyCrypter.decrypt(new EncryptedData(iv, ciphertext), new KeyParameter(key_e));
}
private byte[] sha512(byte[] input) {
SHA512Digest digest = new SHA512Digest();
byte[] hash = new byte[digest.getDigestSize()];
digest.update(input, 0, input.length);
digest.doFinal(hash, 0);
return hash;
}
private byte[] hmac256(byte[] key, byte[] input) {
HMac hmac = new HMac(new SHA256Digest());
hmac.init(new KeyParameter(key));
byte[] result = new byte[hmac.getMacSize()];
hmac.update(input, 0, input.length);
hmac.doFinal(result, 0);
return result;
}
private byte[] concat(byte[] ...bytes) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
for(byte[] byteArray : bytes) {
out.write(byteArray);
}
} catch (IOException e) {
//can't happen
}
return out.toByteArray();
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View file

@ -45,4 +45,9 @@ public final class EncryptedData {
return "EncryptedData [initialisationVector=" + Arrays.toString(initialisationVector) return "EncryptedData [initialisationVector=" + Arrays.toString(initialisationVector)
+ ", encryptedPrivateKey=" + Arrays.toString(encryptedBytes) + "]"; + ", encryptedPrivateKey=" + Arrays.toString(encryptedBytes) + "]";
} }
public EncryptedData copy() {
return new EncryptedData(Arrays.copyOf(initialisationVector, initialisationVector.length),
Arrays.copyOf(encryptedBytes, encryptedBytes.length));
}
} }

View file

@ -1,5 +1,5 @@
package com.sparrowwallet.drongo.crypto; package com.sparrowwallet.drongo.crypto;
public enum EncryptionType { public enum EncryptionType {
UNENCRYPTED, ENCRYPTED_SCRYPT_AES, ENCRYPTED_ECIES; UNENCRYPTED, ENCRYPTED_AES, ENCRYPTED_SCRYPT_AES, ENCRYPTED_ECIES_AES;
} }

View file

@ -38,7 +38,7 @@ public interface KeyCrypter {
* *
* @throws KeyCrypterException if decryption was unsuccessful. * @throws KeyCrypterException if decryption was unsuccessful.
*/ */
byte[] decrypt(EncryptedData encryptedBytesToDecode, KeyParameter aesKey) throws KeyCrypterException; byte[] decrypt(EncryptedData encryptedBytesToDecode, KeyParameter key) throws KeyCrypterException;
/** /**
* Encrypt the supplied bytes, converting them into ciphertext. * Encrypt the supplied bytes, converting them into ciphertext.
@ -46,5 +46,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 aesKey) throws KeyCrypterException; EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, KeyParameter key) throws KeyCrypterException;
} }

View file

@ -1,18 +1,11 @@
package com.sparrowwallet.drongo.crypto; package com.sparrowwallet.drongo.crypto;
import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.generators.SCrypt; import org.bouncycastle.crypto.generators.SCrypt;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Objects; import java.util.Objects;
/** /**
@ -29,7 +22,7 @@ import java.util.Objects;
* <p>2) Using the AES Key generated above, you then can encrypt and decrypt any bytes using * <p>2) Using the AES Key generated above, you then can encrypt and decrypt any bytes using
* the AES symmetric cipher. Eight bytes of salt is used to prevent dictionary attacks.</p> * the AES symmetric cipher. Eight bytes of salt is used to prevent dictionary attacks.</p>
*/ */
public class ScryptKeyCrypter implements KeyCrypter { public class ScryptKeyCrypter extends AESKeyCrypter {
private static final Logger log = LoggerFactory.getLogger(ScryptKeyCrypter.class); private static final Logger log = LoggerFactory.getLogger(ScryptKeyCrypter.class);
/** /**
@ -37,12 +30,6 @@ public class ScryptKeyCrypter implements KeyCrypter {
*/ */
public static final int KEY_LENGTH = 32; // = 256 bits. public static final int KEY_LENGTH = 32; // = 256 bits.
/**
* The size of an AES block in bytes.
* This is also the length of the initialisation vector.
*/
public static final int BLOCK_LENGTH = 16; // = 128 bits.
/** /**
* The length of the salt used. * The length of the salt used.
*/ */
@ -124,72 +111,6 @@ public class ScryptKeyCrypter implements KeyCrypter {
} }
} }
/**
* Password based encryption using AES - CBC 256 bits.
*/
@Override
public EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, KeyParameter aesKey) throws KeyCrypterException {
if(plainBytes == null || aesKey == null) {
throw new KeyCrypterException("Data and key to encrypt cannot be null");
}
try {
// Generate iv - each encryption call has a different iv.
byte[] iv = initializationVector;
if(iv == null) {
iv = new byte[BLOCK_LENGTH];
secureRandom.nextBytes(iv);
}
ParametersWithIV keyWithIv = new ParametersWithIV(aesKey, iv);
// Encrypt using AES.
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
cipher.init(true, keyWithIv);
byte[] encryptedBytes = new byte[cipher.getOutputSize(plainBytes.length)];
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));
} catch (Exception e) {
throw new KeyCrypterException("Could not encrypt bytes.", e);
}
}
/**
* Decrypt bytes previously encrypted with this class.
*
* @param dataToDecrypt The data to decrypt
* @param aesKey The AES key to use for decryption
* @return The decrypted bytes
* @throws KeyCrypterException if bytes could not be decrypted
*/
@Override
public byte[] decrypt(EncryptedData dataToDecrypt, KeyParameter 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());
// Decrypt the message.
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
cipher.init(false, keyWithIv);
byte[] cipherBytes = dataToDecrypt.getEncryptedBytes();
byte[] decryptedBytes = new byte[cipher.getOutputSize(cipherBytes.length)];
final int length1 = cipher.processBytes(cipherBytes, 0, cipherBytes.length, decryptedBytes, 0);
final int length2 = cipher.doFinal(decryptedBytes, length1);
return Arrays.copyOf(decryptedBytes, length1 + length2);
} catch (InvalidCipherTextException e) {
throw new KeyCrypterException.InvalidCipherText("Could not decrypt bytes", e);
} catch (RuntimeException e) {
throw new KeyCrypterException("Could not decrypt bytes", e);
}
}
/** /**
* Convert a CharSequence (which are UTF16) into a byte array. * Convert a CharSequence (which are UTF16) into a byte array.
* *

View file

@ -9,19 +9,16 @@ import org.bouncycastle.crypto.params.KeyParameter;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Arrays; import java.util.*;
import java.util.List;
import java.util.Objects;
import java.util.StringJoiner;
public class DeterministicSeed implements EncryptableItem { public class DeterministicSeed implements EncryptableItem {
public static final int DEFAULT_SEED_ENTROPY_BITS = 128; public static final int DEFAULT_SEED_ENTROPY_BITS = 128;
public static final int MAX_SEED_ENTROPY_BITS = 512; public static final int MAX_SEED_ENTROPY_BITS = 512;
private final byte[] seed; private final byte[] seed;
private final EncryptedData encryptedSeed;
private final List<String> mnemonicCode; private final List<String> mnemonicCode;
private final EncryptedData encryptedSeed;
private final EncryptedData encryptedMnemonicCode; private final EncryptedData encryptedMnemonicCode;
private long creationTimeSeconds; private long creationTimeSeconds;
@ -248,4 +245,12 @@ public class DeterministicSeed implements EncryptableItem {
private static List<String> decodeMnemonicCode(String mnemonicCode) { private static List<String> decodeMnemonicCode(String mnemonicCode) {
return Arrays.asList(mnemonicCode.split(" ")); return Arrays.asList(mnemonicCode.split(" "));
} }
public DeterministicSeed copy() {
if(isEncrypted()) {
return new DeterministicSeed(encryptedMnemonicCode.copy(), encryptedSeed.copy(), creationTimeSeconds);
}
return new DeterministicSeed(Arrays.copyOf(seed, seed.length), new ArrayList<>(mnemonicCode), creationTimeSeconds);
}
} }

View file

@ -17,7 +17,7 @@ public class Keystore {
private WalletModel walletModel = WalletModel.SPARROW; private WalletModel walletModel = WalletModel.SPARROW;
private KeyDerivation keyDerivation; private KeyDerivation keyDerivation;
private ExtendedKey extendedPublicKey; private ExtendedKey extendedPublicKey;
private byte[] seed; private DeterministicSeed seed;
public Keystore() { public Keystore() {
this(DEFAULT_LABEL); this(DEFAULT_LABEL);
@ -71,11 +71,11 @@ public class Keystore {
this.extendedPublicKey = extendedPublicKey; this.extendedPublicKey = extendedPublicKey;
} }
public byte[] getSeed() { public DeterministicSeed getSeed() {
return seed; return seed;
} }
public void setSeed(byte[] seed) { public void setSeed(DeterministicSeed seed) {
this.seed = seed; this.seed = seed;
} }
@ -84,7 +84,11 @@ public class Keystore {
throw new IllegalArgumentException("Keystore does not contain a seed"); throw new IllegalArgumentException("Keystore does not contain a seed");
} }
return HDKeyDerivation.createMasterPrivateKey(seed); if(seed.isEncrypted()) {
throw new IllegalArgumentException("Seed is encrypted");
}
return HDKeyDerivation.createMasterPrivateKey(seed.getSeedBytes());
} }
public ExtendedKey getExtendedPrivateKey() { public ExtendedKey getExtendedPrivateKey() {
@ -119,10 +123,13 @@ public class Keystore {
if(extendedPublicKey != null) { if(extendedPublicKey != null) {
copy.setExtendedPublicKey(extendedPublicKey.copy()); copy.setExtendedPublicKey(extendedPublicKey.copy());
} }
if(seed != null) {
copy.setSeed(seed.copy());
}
return copy; return copy;
} }
public static Keystore fromSeed(byte[] seed, List<ChildNumber> derivation) { public static Keystore fromSeed(DeterministicSeed seed, List<ChildNumber> derivation) {
Keystore keystore = new Keystore(); Keystore keystore = new Keystore();
keystore.setSeed(seed); keystore.setSeed(seed);
ExtendedKey xprv = keystore.getExtendedPrivateKey(); ExtendedKey xprv = keystore.getExtendedPrivateKey();

View file

@ -0,0 +1,24 @@
package com.sparrowwallet.drongo.crypto;
import org.junit.Assert;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
public class ECIESKeyCrypterTest {
@Test
public void encryptDecrypt() {
String testMessage = "thisisatestmessage";
byte[] testMessageBytes = testMessage.getBytes(StandardCharsets.UTF_8);
byte[] initializationVector = "BIE1".getBytes(StandardCharsets.UTF_8);
AsymmetricKeyCrypter keyCrypter = new ECIESKeyCrypter();
ECKey key = keyCrypter.deriveECKey("iampassword");
EncryptedData encryptedData = keyCrypter.encrypt(testMessageBytes, initializationVector, key);
byte[] crypterDecrypted = keyCrypter.decrypt(encryptedData, key);
String cryDecStr = new String(crypterDecrypted, StandardCharsets.UTF_8);
Assert.assertEquals(testMessage, cryDecStr);
}
}

View file

@ -0,0 +1,40 @@
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() {
Security.addProvider(new BouncyCastleProvider());
KeyCrypter keyDeriver = new AESKeyCrypter();
KeyParameter keyParameter = keyDeriver.deriveKey("password");
String message = "testastringmessage";
byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
byte[] iv = new byte[16];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(iv);
ScryptKeyCrypter scryptKeyCrypter = new ScryptKeyCrypter();
EncryptedData scrypted = scryptKeyCrypter.encrypt(messageBytes, iv, keyParameter);
AESKeyCrypter aesKeyCrypter = new AESKeyCrypter();
EncryptedData aescrypted = aesKeyCrypter.encrypt(messageBytes, iv, keyParameter);
Assert.assertArrayEquals(scrypted.getEncryptedBytes(), aescrypted.getEncryptedBytes());
byte[] sdecrypted = scryptKeyCrypter.decrypt(scrypted, keyParameter);
byte[] aesdecrypted = aesKeyCrypter.decrypt(aescrypted, keyParameter);
Assert.assertArrayEquals(sdecrypted, aesdecrypted);
}
}

View file

@ -1,20 +0,0 @@
package com.sparrowwallet.drongo.protocol;
import com.sparrowwallet.drongo.crypto.ECKey;
import org.junit.Assert;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
public class ECKeyTest {
@Test
public void encryptDecrypt() {
String testMessage = "thisisatestmessage";
ECKey pubKey = ECKey.createKeyPbkdf2HmacSha512("iampassword");
byte[] encrypted = pubKey.encryptEcies(testMessage.getBytes(StandardCharsets.UTF_8), "BIE1".getBytes(StandardCharsets.UTF_8));
byte[] decrypted = pubKey.decryptEcies(encrypted, "BIE1".getBytes(StandardCharsets.UTF_8));
String decStr = new String(decrypted, StandardCharsets.UTF_8);
Assert.assertEquals(testMessage, decStr);
}
}

View file

@ -5,11 +5,14 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import java.util.Collections;
public class KeystoreTest { public class KeystoreTest {
@Test @Test
public void testExtendedPrivateKey() { public void testExtendedPrivateKey() {
Keystore keystore = new Keystore(); Keystore keystore = new Keystore();
keystore.setSeed(Utils.hexToBytes("727ecfcf0bce9d8ec0ef066f7aeb845c271bdd4ee06a37398cebd40dc810140bb620b6c10a8ad671afdceaf37aa55d92d6478f747e8b92430dd938ab5be961dd")); DeterministicSeed seed = new DeterministicSeed(Utils.hexToBytes("727ecfcf0bce9d8ec0ef066f7aeb845c271bdd4ee06a37398cebd40dc810140bb620b6c10a8ad671afdceaf37aa55d92d6478f747e8b92430dd938ab5be961dd"), Collections.emptyList(), 0);
keystore.setSeed(seed);
Assert.assertEquals("xprv9s21ZrQH143K3rN5vhm4bKDKsk1PmUK1mzxSMwkVSp2GbomwGmjLaGqrs8Nn9r14jCsfCNWfTR6pAtCsJutUH6QSHX65CePNW3YVyGxqvJa", keystore.getExtendedPrivateKey().toString()); Assert.assertEquals("xprv9s21ZrQH143K3rN5vhm4bKDKsk1PmUK1mzxSMwkVSp2GbomwGmjLaGqrs8Nn9r14jCsfCNWfTR6pAtCsJutUH6QSHX65CePNW3YVyGxqvJa", keystore.getExtendedPrivateKey().toString());
} }
@ -17,7 +20,8 @@ public class KeystoreTest {
@Test @Test
public void testExtendedPrivateKeyTwo() { public void testExtendedPrivateKeyTwo() {
Keystore keystore = new Keystore(); Keystore keystore = new Keystore();
keystore.setSeed(Utils.hexToBytes("4d8c47d0d6241169d0b17994219211c4a980f7146bb70dbc897416790e9de23a6265c708b88176e24f6eb7378a7c55cd4bdc067cafe574eaf3480f9a41c3c43c")); DeterministicSeed seed = new DeterministicSeed(Utils.hexToBytes("4d8c47d0d6241169d0b17994219211c4a980f7146bb70dbc897416790e9de23a6265c708b88176e24f6eb7378a7c55cd4bdc067cafe574eaf3480f9a41c3c43c"), Collections.emptyList(), 0);
keystore.setSeed(seed);
Assert.assertEquals("xprv9s21ZrQH143K4AkrxAyivDeTCWhZV6fdLfBRR8QerWe9hHiRqMjBMj9MFNef7oFufgcDcW54LhguPNm6MVLEMWPX5qxKhmNjCzi9Zy6yhkc", keystore.getExtendedPrivateKey().toString()); Assert.assertEquals("xprv9s21ZrQH143K4AkrxAyivDeTCWhZV6fdLfBRR8QerWe9hHiRqMjBMj9MFNef7oFufgcDcW54LhguPNm6MVLEMWPX5qxKhmNjCzi9Zy6yhkc", keystore.getExtendedPrivateKey().toString());
} }
@ -25,7 +29,8 @@ public class KeystoreTest {
@Test @Test
public void testFromSeed() { public void testFromSeed() {
ScriptType p2pkh = ScriptType.P2PKH; ScriptType p2pkh = ScriptType.P2PKH;
Keystore keystore = Keystore.fromSeed(Utils.hexToBytes("4d8c47d0d6241169d0b17994219211c4a980f7146bb70dbc897416790e9de23a6265c708b88176e24f6eb7378a7c55cd4bdc067cafe574eaf3480f9a41c3c43c"), p2pkh.getDefaultDerivation()); DeterministicSeed seed = new DeterministicSeed(Utils.hexToBytes("4d8c47d0d6241169d0b17994219211c4a980f7146bb70dbc897416790e9de23a6265c708b88176e24f6eb7378a7c55cd4bdc067cafe574eaf3480f9a41c3c43c"), Collections.emptyList(), 0);
Keystore keystore = Keystore.fromSeed(seed, p2pkh.getDefaultDerivation());
Assert.assertEquals("xpub6DCH2YkjweBu5zQheCWgSu6o26AENhApkS2taXaJBsi6vthRytPTaY2Sh4zDHj7oCVhYxx5974HbSbKxh26ah7N6VVw1U8kS2H5HfPUXecq", keystore.getExtendedPublicKey().toString()); Assert.assertEquals("xpub6DCH2YkjweBu5zQheCWgSu6o26AENhApkS2taXaJBsi6vthRytPTaY2Sh4zDHj7oCVhYxx5974HbSbKxh26ah7N6VVw1U8kS2H5HfPUXecq", keystore.getExtendedPublicKey().toString());
} }