diff --git a/src/main/java/com/sparrowwallet/drongo/SecureString.java b/src/main/java/com/sparrowwallet/drongo/SecureString.java new file mode 100644 index 0000000..80d8edd --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/SecureString.java @@ -0,0 +1,100 @@ +package com.sparrowwallet.drongo; + +import java.security.SecureRandom; +import java.util.Arrays; + +/** + * This is not a string but a CharSequence that can be cleared of its memory. + * Important for handling passwords. Represents text that should be kept + * confidential, such as by deleting it from computer memory when no longer + * needed or garbaged collected. + */ +public class SecureString implements CharSequence { + + private final int[] chars; + private final int[] pad; + + public SecureString(final CharSequence original) { + this(0, original.length(), original); + } + + public SecureString(final int start, final int end, final CharSequence original) { + final int length = end - start; + pad = new int[length]; + chars = new int[length]; + scramble(start, length, original); + } + + @Override + public char charAt(final int i) { + return (char) (pad[i] ^ chars[i]); + } + + @Override + public int length() { + return chars.length; + } + + @Override + public CharSequence subSequence(final int start, final int end) { + return new SecureString(start, end, this); + } + + /** + * Convert array back to String but not using toString(). See toString() docs + * below. + */ + public String asString() { + final char[] value = new char[chars.length]; + for (int i = 0; i < value.length; i++) { + value[i] = charAt(i); + } + return new String(value); + } + + /** + * Manually clear the underlying array holding the characters + */ + public void clear() { + Arrays.fill(chars, '0'); + Arrays.fill(pad, 0); + } + + /** + * Protect against using this class in log statements. + *

+ * {@inheritDoc} + */ + @Override + public String toString() { + return "Secure:XXXXX"; + } + + /** + * Called by garbage collector. + *

+ * {@inheritDoc} + */ + @SuppressWarnings("deprecation") + @Override + public void finalize() throws Throwable { + clear(); + super.finalize(); + } + + /** + * Randomly pad the characters to not store the real character in memory. + * + * @param start start of the {@code CharSequence} + * @param length length of the {@code CharSequence} + * @param characters the {@code CharSequence} to scramble + */ + private void scramble(final int start, final int length, final CharSequence characters) { + final SecureRandom random = new SecureRandom(); + for (int i = start; i < length; i++) { + final char charAt = characters.charAt(i); + pad[i] = random.nextInt(); + chars[i] = pad[i] ^ charAt; + } + } +} diff --git a/src/main/java/com/sparrowwallet/drongo/Utils.java b/src/main/java/com/sparrowwallet/drongo/Utils.java index 6d37401..72db457 100644 --- a/src/main/java/com/sparrowwallet/drongo/Utils.java +++ b/src/main/java/com/sparrowwallet/drongo/Utils.java @@ -5,7 +5,6 @@ import com.sparrowwallet.drongo.protocol.ProtocolException; import com.sparrowwallet.drongo.protocol.Ripemd160; import com.sparrowwallet.drongo.protocol.Sha256Hash; import org.bouncycastle.crypto.digests.SHA512Digest; -import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator; import org.bouncycastle.crypto.macs.HMac; import org.bouncycastle.crypto.params.KeyParameter; @@ -13,6 +12,10 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.math.BigInteger; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; import java.util.*; public class Utils { @@ -280,4 +283,40 @@ public class Utils { hmacSha512.doFinal(out, 0); return out; } + + public static byte[] toBytesUTF8(CharSequence charSequence) { + CharBuffer charBuffer = CharBuffer.wrap(charSequence); + ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer); + byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); + Arrays.fill(byteBuffer.array(), (byte)0); // clear sensitive data + return bytes; + } + + public static SecureString fromBytesUTF8(byte[] bytes) { + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + CharBuffer charBuffer = StandardCharsets.UTF_8.decode(byteBuffer); + SecureString secureString = new SecureString(charBuffer); + Arrays.fill(charBuffer.array(), (char)0); + return secureString; + } + + public static byte[] toBytesUTF16(CharSequence charSequence) { + byte[] byteArray = new byte[charSequence.length() << 1]; + for(int i = 0; i < charSequence.length(); i++) { + int bytePosition = i << 1; + byteArray[bytePosition] = (byte) ((charSequence.charAt(i)&0xFF00)>>8); + byteArray[bytePosition + 1] = (byte) (charSequence.charAt(i)&0x00FF); + } + return byteArray; + } + + public static boolean isValidUTF16(CharSequence charSequence) { + for (int i = 0; i < charSequence.length(); i++) { + if (Character.isLowSurrogate(charSequence.charAt(i)) && (i == 0 || !Character.isHighSurrogate(charSequence.charAt(i - 1))) + || Character.isHighSurrogate(charSequence.charAt(i)) && (i == charSequence.length() -1 || !Character.isLowSurrogate(charSequence.charAt(i + 1)))) { + return false; + } + } + return true; + } } diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/Argon2KeyDeriver.java b/src/main/java/com/sparrowwallet/drongo/crypto/Argon2KeyDeriver.java index e8c5229..7e4c46c 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/Argon2KeyDeriver.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/Argon2KeyDeriver.java @@ -1,9 +1,9 @@ package com.sparrowwallet.drongo.crypto; +import com.sparrowwallet.drongo.Utils; import de.mkammerer.argon2.Argon2Advanced; import de.mkammerer.argon2.Argon2Factory; -import java.nio.charset.StandardCharsets; import java.security.SecureRandom; public class Argon2KeyDeriver implements KeyDeriver, AsymmetricKeyDeriver { @@ -40,14 +40,14 @@ public class Argon2KeyDeriver implements KeyDeriver, AsymmetricKeyDeriver { } @Override - public Key deriveKey(String password) throws KeyCrypterException { + public Key deriveKey(CharSequence password) throws KeyCrypterException { Argon2Advanced argon2 = Argon2Factory.createAdvanced(Argon2Factory.Argon2Types.ARGON2id, argon2Parameters.saltLength, argon2Parameters.hashLength); - byte[] hash = argon2.rawHash(argon2Parameters.iterations, argon2Parameters.memory, argon2Parameters.parallelism, password.getBytes(StandardCharsets.UTF_8), salt); + byte[] hash = argon2.rawHash(argon2Parameters.iterations, argon2Parameters.memory, argon2Parameters.parallelism, Utils.toBytesUTF8(password), salt); return new Key(hash, salt, getDeriverType()); } @Override - public ECKey deriveECKey(String password) throws KeyCrypterException { + public ECKey deriveECKey(CharSequence password) throws KeyCrypterException { Key key = deriveKey(password); return ECKey.fromPrivate(key.getKeyBytes()); } diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/AsymmetricKeyDeriver.java b/src/main/java/com/sparrowwallet/drongo/crypto/AsymmetricKeyDeriver.java index 4a9c8a7..6daee4f 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/AsymmetricKeyDeriver.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/AsymmetricKeyDeriver.java @@ -7,7 +7,7 @@ public interface AsymmetricKeyDeriver { * @return ECKey The ECKey to use for encrypting and decrypting * @throws KeyCrypterException */ - ECKey deriveECKey(String password) throws KeyCrypterException; + ECKey deriveECKey(CharSequence password) throws KeyCrypterException; byte[] getSalt(); } diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/DoubleSha256KeyDeriver.java b/src/main/java/com/sparrowwallet/drongo/crypto/DoubleSha256KeyDeriver.java index 9f358a2..abda797 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/DoubleSha256KeyDeriver.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/DoubleSha256KeyDeriver.java @@ -1,5 +1,6 @@ package com.sparrowwallet.drongo.crypto; +import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.protocol.Sha256Hash; import java.nio.charset.StandardCharsets; @@ -7,8 +8,9 @@ import java.nio.charset.StandardCharsets; public class DoubleSha256KeyDeriver implements KeyDeriver { @Override - public Key deriveKey(String password) throws KeyCrypterException { - byte[] sha256 = Sha256Hash.hash(password.getBytes(StandardCharsets.UTF_8)); + public Key deriveKey(CharSequence password) throws KeyCrypterException { + byte[] passwordBytes = Utils.toBytesUTF8(password); + byte[] sha256 = Sha256Hash.hash(passwordBytes); byte[] doubleSha256 = Sha256Hash.hash(sha256); return new Key(doubleSha256, null, getDeriverType()); } diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/EncryptionType.java b/src/main/java/com/sparrowwallet/drongo/crypto/EncryptionType.java index 9e6a0eb..f137623 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/EncryptionType.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/EncryptionType.java @@ -1,5 +1,7 @@ package com.sparrowwallet.drongo.crypto; +import com.sparrowwallet.drongo.Utils; + import java.nio.charset.StandardCharsets; import java.util.Objects; @@ -9,8 +11,8 @@ public class EncryptionType { public KeyDeriver getKeyDeriver() { return new KeyDeriver() { @Override - public Key deriveKey(String password) throws KeyCrypterException { - return new Key(password.getBytes(StandardCharsets.UTF_8), null, NONE); + public Key deriveKey(CharSequence password) throws KeyCrypterException { + return new Key(Utils.toBytesUTF8(password), null, NONE); } @Override diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/KeyDeriver.java b/src/main/java/com/sparrowwallet/drongo/crypto/KeyDeriver.java index 2bbd2aa..3d9c855 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/KeyDeriver.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/KeyDeriver.java @@ -7,7 +7,7 @@ public interface KeyDeriver { * @return Key The Key which typically contains the AES key to use for encrypting and decrypting * @throws KeyCrypterException */ - Key deriveKey(String password) throws KeyCrypterException; + Key deriveKey(CharSequence password) throws KeyCrypterException; EncryptionType.Deriver getDeriverType(); } diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/Pbkdf2KeyDeriver.java b/src/main/java/com/sparrowwallet/drongo/crypto/Pbkdf2KeyDeriver.java index 497757f..335584a 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/Pbkdf2KeyDeriver.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/Pbkdf2KeyDeriver.java @@ -1,11 +1,10 @@ package com.sparrowwallet.drongo.crypto; +import com.sparrowwallet.drongo.Utils; import org.bouncycastle.crypto.digests.SHA512Digest; import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator; import org.bouncycastle.crypto.params.KeyParameter; -import java.nio.charset.StandardCharsets; - public class Pbkdf2KeyDeriver implements KeyDeriver, AsymmetricKeyDeriver { public static final int DEFAULT_ITERATION_COUNT = 1024; @@ -35,15 +34,15 @@ public class Pbkdf2KeyDeriver implements KeyDeriver, AsymmetricKeyDeriver { } @Override - public Key deriveKey(String password) throws KeyCrypterException { + public Key deriveKey(CharSequence password) throws KeyCrypterException { PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA512Digest()); - gen.init(password.getBytes(StandardCharsets.UTF_8), salt, iterationCount); + gen.init(Utils.toBytesUTF8(password), salt, iterationCount); byte[] keyBytes = ((KeyParameter)gen.generateDerivedParameters(512)).getKey(); return new Key(keyBytes, salt, getDeriverType()); } @Override - public ECKey deriveECKey(String password) throws KeyCrypterException { + public ECKey deriveECKey(CharSequence password) throws KeyCrypterException { Key key = deriveKey(password); return ECKey.fromPrivate(key.getKeyBytes()); } diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/ScryptKeyDeriver.java b/src/main/java/com/sparrowwallet/drongo/crypto/ScryptKeyDeriver.java index a8a475d..c24c4bb 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/ScryptKeyDeriver.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/ScryptKeyDeriver.java @@ -1,5 +1,6 @@ package com.sparrowwallet.drongo.crypto; +import com.sparrowwallet.drongo.Utils; import org.bouncycastle.crypto.generators.SCrypt; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -99,10 +100,10 @@ public class ScryptKeyDeriver implements KeyDeriver { * @throws KeyCrypterException */ @Override - public Key deriveKey(String password) throws KeyCrypterException { + public Key deriveKey(CharSequence password) throws KeyCrypterException { byte[] passwordBytes = null; try { - passwordBytes = convertToByteArray(password); + passwordBytes = Utils.toBytesUTF8(password); byte[] salt = new byte[0]; if (scryptParameters.getSalt() != null) { salt = scryptParameters.getSalt(); @@ -122,21 +123,6 @@ public class ScryptKeyDeriver implements KeyDeriver { } } - /** - * Convert a CharSequence (which are UTF16) into a byte array. - * - * Note: a String.getBytes() is not used to avoid creating a String of the password in the JVM. - */ - private static byte[] convertToByteArray(CharSequence charSequence) { - byte[] byteArray = new byte[charSequence.length() << 1]; - for(int i = 0; i < charSequence.length(); i++) { - int bytePosition = i << 1; - byteArray[bytePosition] = (byte) ((charSequence.charAt(i)&0xFF00)>>8); - byteArray[bytePosition + 1] = (byte) (charSequence.charAt(i)&0x00FF); - } - return byteArray; - } - public ScryptParameters getScryptParameters() { return scryptParameters; } diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java b/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java index 948246f..04db86b 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/DeterministicSeed.java @@ -199,7 +199,7 @@ public class DeterministicSeed implements EncryptableItem { return mnemonicString.getBytes(StandardCharsets.UTF_8); } - public DeterministicSeed decrypt(String password) { + public DeterministicSeed decrypt(CharSequence password) { if(!isEncrypted()) { throw new IllegalStateException("Cannot decrypt unencrypted seed"); } diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java b/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java index ce388c8..7beb7f3 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java @@ -191,7 +191,7 @@ public class Keystore { } } - public void decrypt(String password) { + public void decrypt(CharSequence password) { if(hasSeed() && seed.isEncrypted()) { seed = seed.decrypt(password); } diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java index 5e768a7..f226bd0 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java @@ -187,7 +187,7 @@ public class Wallet { } } - public void decrypt(String password) { + public void decrypt(CharSequence password) { for(Keystore keystore : keystores) { keystore.decrypt(password); }