From 492f447c28f91733dfea685dbe87ff948e7922b2 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 11 May 2020 17:39:23 +0200 Subject: [PATCH] refactor keycrypter hierarchy, add ECIESKeyCrypter --- .../drongo/crypto/AESKeyCrypter.java | 112 +++++++++------ .../drongo/crypto/AsymmetricKeyCrypter.java | 32 +++++ .../drongo/crypto/ECIESKeyCrypter.java | 128 ++++++++++++++++++ .../sparrowwallet/drongo/crypto/ECKey.java | 102 -------------- .../drongo/crypto/EncryptedData.java | 5 + .../drongo/crypto/EncryptionType.java | 2 +- .../drongo/crypto/KeyCrypter.java | 4 +- .../drongo/crypto/ScryptKeyCrypter.java | 81 +---------- .../drongo/wallet/DeterministicSeed.java | 17 ++- .../sparrowwallet/drongo/wallet/Keystore.java | 17 ++- .../drongo/crypto/ECIESKeyCrypterTest.java | 24 ++++ .../drongo/crypto/ScryptKeyCrypterTest.java | 40 ++++++ .../drongo/protocol/ECKeyTest.java | 20 --- .../drongo/wallet/KeystoreTest.java | 11 +- 14 files changed, 337 insertions(+), 258 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/drongo/crypto/AsymmetricKeyCrypter.java create mode 100644 src/main/java/com/sparrowwallet/drongo/crypto/ECIESKeyCrypter.java create mode 100644 src/test/java/com/sparrowwallet/drongo/crypto/ECIESKeyCrypterTest.java create mode 100644 src/test/java/com/sparrowwallet/drongo/crypto/ScryptKeyCrypterTest.java delete mode 100644 src/test/java/com/sparrowwallet/drongo/protocol/ECKeyTest.java diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/AESKeyCrypter.java b/src/main/java/com/sparrowwallet/drongo/crypto/AESKeyCrypter.java index 01d10b9..53b297e 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/AESKeyCrypter.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/AESKeyCrypter.java @@ -1,67 +1,101 @@ 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.ParametersWithIV; -import javax.crypto.Cipher; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.StandardCharsets; -import java.security.spec.AlgorithmParameterSpec; +import java.security.SecureRandom; +import java.util.Arrays; +/* + * + */ 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 public EncryptionType getUnderstoodEncryptionType() { - return null; + return EncryptionType.ENCRYPTED_AES; } @Override public KeyParameter deriveKey(CharSequence password) throws KeyCrypterException { - return createKeyPbkdf2HmacSha512(password.toString()); - } - - 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); + throw new UnsupportedOperationException("AESKeyCrypter does not define a key derivation function, but keys must be either 128, 192 or 256 bits long"); } + /** + * 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 encryptedBytesToDecode, KeyParameter aesKey) throws KeyCrypterException { - return decryptAesCbcPkcs7(encryptedBytesToDecode.getEncryptedBytes(), encryptedBytesToDecode.getInitialisationVector(), aesKey.getKey()); - } + 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"); + } - private byte[] decryptAesCbcPkcs7(byte[] ciphertext, byte[] iv, byte[] key_e) { try { - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); - SecretKeySpec secretKeySpec = new SecretKeySpec(key_e, "AES"); - AlgorithmParameterSpec paramSpec = new IvParameterSpec(iv); - cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, paramSpec); - return cipher.doFinal(ciphertext); - } catch (Exception e) { - throw new KeyCrypterException("Error decrypting", e); + 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); } } + /** + * Password based encryption using AES - CBC 256 bits. + */ @Override public EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, KeyParameter aesKey) throws KeyCrypterException { - byte[] encryptedData = encryptAesCbcPkcs7(plainBytes, initializationVector, aesKey.getKey()); - return new EncryptedData(initializationVector, encryptedData); - } + if(plainBytes == null || aesKey == null) { + throw new KeyCrypterException("Data and key to encrypt cannot be null"); + } - private byte[] encryptAesCbcPkcs7(byte[] message, byte[] iv, byte[] key_e) { try { - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); - SecretKeySpec secretKeySpec = new SecretKeySpec(key_e, "AES"); - AlgorithmParameterSpec paramSpec = new IvParameterSpec(iv); - cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, paramSpec); - return cipher.doFinal(message); - } catch(Exception e) { - throw new KeyCrypterException("Could not encrypt", e); + // 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); } } } diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/AsymmetricKeyCrypter.java b/src/main/java/com/sparrowwallet/drongo/crypto/AsymmetricKeyCrypter.java new file mode 100644 index 0000000..a83cb1c --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/crypto/AsymmetricKeyCrypter.java @@ -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; +} diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/ECIESKeyCrypter.java b/src/main/java/com/sparrowwallet/drongo/crypto/ECIESKeyCrypter.java new file mode 100644 index 0000000..5cb55bc --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/crypto/ECIESKeyCrypter.java @@ -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 { + } +} diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/ECKey.java b/src/main/java/com/sparrowwallet/drongo/crypto/ECKey.java index d3aa94e..8fc8f13 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/ECKey.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/ECKey.java @@ -97,8 +97,6 @@ public class ECKey implements EncryptableItem { CURVE_PARAMS.getH()); HALF_CURVE_ORDER = CURVE_PARAMS.getN().shiftRight(1); 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. @@ -879,106 +877,6 @@ public class ECKey implements EncryptableItem { 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 public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/EncryptedData.java b/src/main/java/com/sparrowwallet/drongo/crypto/EncryptedData.java index a5ac612..e197e67 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/EncryptedData.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/EncryptedData.java @@ -45,4 +45,9 @@ public final class EncryptedData { return "EncryptedData [initialisationVector=" + Arrays.toString(initialisationVector) + ", encryptedPrivateKey=" + Arrays.toString(encryptedBytes) + "]"; } + + public EncryptedData copy() { + return new EncryptedData(Arrays.copyOf(initialisationVector, initialisationVector.length), + Arrays.copyOf(encryptedBytes, encryptedBytes.length)); + } } diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/EncryptionType.java b/src/main/java/com/sparrowwallet/drongo/crypto/EncryptionType.java index 18b36fc..09ec194 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/EncryptionType.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/EncryptionType.java @@ -1,5 +1,5 @@ package com.sparrowwallet.drongo.crypto; public enum EncryptionType { - UNENCRYPTED, ENCRYPTED_SCRYPT_AES, ENCRYPTED_ECIES; + UNENCRYPTED, ENCRYPTED_AES, ENCRYPTED_SCRYPT_AES, ENCRYPTED_ECIES_AES; } diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/KeyCrypter.java b/src/main/java/com/sparrowwallet/drongo/crypto/KeyCrypter.java index e390fbf..6759438 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/KeyCrypter.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/KeyCrypter.java @@ -38,7 +38,7 @@ public interface KeyCrypter { * * @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. @@ -46,5 +46,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 aesKey) throws KeyCrypterException; + EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, KeyParameter key) throws KeyCrypterException; } diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/ScryptKeyCrypter.java b/src/main/java/com/sparrowwallet/drongo/crypto/ScryptKeyCrypter.java index 42b2ddf..d778f3a 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/ScryptKeyCrypter.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/ScryptKeyCrypter.java @@ -1,18 +1,11 @@ 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.modes.CBCBlockCipher; -import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; import org.bouncycastle.crypto.params.KeyParameter; -import org.bouncycastle.crypto.params.ParametersWithIV; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.security.SecureRandom; -import java.util.Arrays; import java.util.Objects; /** @@ -29,7 +22,7 @@ import java.util.Objects; *

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.

*/ -public class ScryptKeyCrypter implements KeyCrypter { +public class ScryptKeyCrypter extends AESKeyCrypter { 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. - /** - * 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. */ @@ -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. * diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java b/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java index 0cba865..342a1c7 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java @@ -9,19 +9,16 @@ import org.bouncycastle.crypto.params.KeyParameter; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.StringJoiner; +import java.util.*; public class DeterministicSeed implements EncryptableItem { public static final int DEFAULT_SEED_ENTROPY_BITS = 128; public static final int MAX_SEED_ENTROPY_BITS = 512; private final byte[] seed; - private final EncryptedData encryptedSeed; - private final List mnemonicCode; + + private final EncryptedData encryptedSeed; private final EncryptedData encryptedMnemonicCode; private long creationTimeSeconds; @@ -248,4 +245,12 @@ public class DeterministicSeed implements EncryptableItem { private static List decodeMnemonicCode(String mnemonicCode) { 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); + } } diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java b/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java index 5dd83f8..f4a66f5 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java @@ -17,7 +17,7 @@ public class Keystore { private WalletModel walletModel = WalletModel.SPARROW; private KeyDerivation keyDerivation; private ExtendedKey extendedPublicKey; - private byte[] seed; + private DeterministicSeed seed; public Keystore() { this(DEFAULT_LABEL); @@ -71,11 +71,11 @@ public class Keystore { this.extendedPublicKey = extendedPublicKey; } - public byte[] getSeed() { + public DeterministicSeed getSeed() { return seed; } - public void setSeed(byte[] seed) { + public void setSeed(DeterministicSeed seed) { this.seed = seed; } @@ -84,7 +84,11 @@ public class Keystore { 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() { @@ -119,10 +123,13 @@ public class Keystore { if(extendedPublicKey != null) { copy.setExtendedPublicKey(extendedPublicKey.copy()); } + if(seed != null) { + copy.setSeed(seed.copy()); + } return copy; } - public static Keystore fromSeed(byte[] seed, List derivation) { + public static Keystore fromSeed(DeterministicSeed seed, List derivation) { Keystore keystore = new Keystore(); keystore.setSeed(seed); ExtendedKey xprv = keystore.getExtendedPrivateKey(); diff --git a/src/test/java/com/sparrowwallet/drongo/crypto/ECIESKeyCrypterTest.java b/src/test/java/com/sparrowwallet/drongo/crypto/ECIESKeyCrypterTest.java new file mode 100644 index 0000000..09b0bd6 --- /dev/null +++ b/src/test/java/com/sparrowwallet/drongo/crypto/ECIESKeyCrypterTest.java @@ -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); + } +} diff --git a/src/test/java/com/sparrowwallet/drongo/crypto/ScryptKeyCrypterTest.java b/src/test/java/com/sparrowwallet/drongo/crypto/ScryptKeyCrypterTest.java new file mode 100644 index 0000000..b6aae38 --- /dev/null +++ b/src/test/java/com/sparrowwallet/drongo/crypto/ScryptKeyCrypterTest.java @@ -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); + } +} diff --git a/src/test/java/com/sparrowwallet/drongo/protocol/ECKeyTest.java b/src/test/java/com/sparrowwallet/drongo/protocol/ECKeyTest.java deleted file mode 100644 index 6c63c9b..0000000 --- a/src/test/java/com/sparrowwallet/drongo/protocol/ECKeyTest.java +++ /dev/null @@ -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); - } -} diff --git a/src/test/java/com/sparrowwallet/drongo/wallet/KeystoreTest.java b/src/test/java/com/sparrowwallet/drongo/wallet/KeystoreTest.java index 82edab2..8d2b661 100644 --- a/src/test/java/com/sparrowwallet/drongo/wallet/KeystoreTest.java +++ b/src/test/java/com/sparrowwallet/drongo/wallet/KeystoreTest.java @@ -5,11 +5,14 @@ import com.sparrowwallet.drongo.protocol.ScriptType; import org.junit.Assert; import org.junit.Test; +import java.util.Collections; + public class KeystoreTest { @Test public void testExtendedPrivateKey() { 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()); } @@ -17,7 +20,8 @@ public class KeystoreTest { @Test public void testExtendedPrivateKeyTwo() { 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()); } @@ -25,7 +29,8 @@ public class KeystoreTest { @Test public void testFromSeed() { 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()); }