encryptableitem and bip39 from bitcoinj

This commit is contained in:
Craig Raw 2020-05-10 13:28:16 +02:00
parent be0c4d1176
commit dc569979e1
13 changed files with 1252 additions and 174 deletions

View file

@ -0,0 +1,67 @@
package com.sparrowwallet.drongo.crypto;
import com.sparrowwallet.drongo.Utils;
import org.bouncycastle.crypto.params.KeyParameter;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.spec.AlgorithmParameterSpec;
public class AESKeyCrypter implements KeyCrypter {
@Override
public EncryptionType getUnderstoodEncryptionType() {
return null;
}
@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);
}
@Override
public byte[] decrypt(EncryptedData encryptedBytesToDecode, KeyParameter aesKey) throws KeyCrypterException {
return decryptAesCbcPkcs7(encryptedBytesToDecode.getEncryptedBytes(), encryptedBytesToDecode.getInitialisationVector(), aesKey.getKey());
}
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);
}
}
@Override
public EncryptedData encrypt(byte[] plainBytes, byte[] initializationVector, KeyParameter aesKey) throws KeyCrypterException {
byte[] encryptedData = encryptAesCbcPkcs7(plainBytes, initializationVector, aesKey.getKey());
return new EncryptedData(initializationVector, encryptedData);
}
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);
}
}
}

View file

@ -10,7 +10,6 @@ import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.digests.SHA512Digest; 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.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.macs.HMac; 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;
@ -24,17 +23,14 @@ import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.*; import java.security.*;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Comparator;
import java.util.Objects; import java.util.Objects;
/** /**
@ -65,9 +61,21 @@ import java.util.Objects;
* this class so round-tripping preserves state. Unless you're working with old software or doing unusual things, you * this class so round-tripping preserves state. Unless you're working with old software or doing unusual things, you
* can usually ignore the compressed/uncompressed distinction.</p> * can usually ignore the compressed/uncompressed distinction.</p>
*/ */
public class ECKey { public class ECKey implements EncryptableItem {
private static final Logger log = LoggerFactory.getLogger(ECKey.class); private static final Logger log = LoggerFactory.getLogger(ECKey.class);
/** Sorts oldest keys first, newest last. */
public static final Comparator<ECKey> AGE_COMPARATOR = new Comparator<ECKey>() {
@Override
public int compare(ECKey k1, ECKey k2) {
if (k1.creationTimeSeconds == k2.creationTimeSeconds)
return 0;
else
return k1.creationTimeSeconds > k2.creationTimeSeconds ? 1 : -1;
}
};
// The parameters of the secp256k1 curve that Bitcoin uses. // The parameters of the secp256k1 curve that Bitcoin uses.
private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1"); private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
@ -101,6 +109,9 @@ public class ECKey {
// not have this field. // not have this field.
protected long creationTimeSeconds; protected long creationTimeSeconds;
protected KeyCrypter keyCrypter;
protected EncryptedData encryptedPrivateKey;
private byte[] pubKeyHash; private byte[] pubKeyHash;
/** /**
@ -145,6 +156,18 @@ public class ECKey {
this.pub = pub; this.pub = pub;
} }
/**
* Constructs a key that has an encrypted private component. The given object wraps encrypted bytes and an
* initialization vector. Note that the key will not be decrypted during this call: the returned ECKey is
* unusable for signing unless a decryption key is supplied.
*/
public static ECKey fromEncrypted(EncryptedData encryptedPrivateKey, KeyCrypter crypter, byte[] pubKey) {
ECKey key = fromPublicOnly(pubKey);
key.encryptedPrivateKey = encryptedPrivateKey;
key.keyCrypter = crypter;
return key;
}
/** /**
* Utility for compressing an elliptic curve point. Returns the same point if it's already compressed. * Utility for compressing an elliptic curve point. Returns the same point if it's already compressed.
* See the ECKey class docs for a discussion of point compression. * See the ECKey class docs for a discussion of point compression.
@ -234,6 +257,19 @@ public class ECKey {
return priv == null; return priv == null;
} }
/**
* Returns true if this key has unencrypted access to private key bytes. Does the opposite of
* {@link #isPubKeyOnly()}.
*/
public boolean hasPrivKey() {
return priv != null;
}
/** Returns true if this key is watch only, meaning it has a public key but no private key. */
public boolean isWatching() {
return isPubKeyOnly() && !isEncrypted();
}
/** /**
* Output this ECKey as an ASN.1 encoded private key, as understood by OpenSSL or used by Bitcoin Core * Output this ECKey as an ASN.1 encoded private key, as understood by OpenSSL or used by Bitcoin Core
* in its wallet storage format. * in its wallet storage format.
@ -313,8 +349,10 @@ public class ECKey {
* @throws java.lang.IllegalStateException if the private key bytes are not available. * @throws java.lang.IllegalStateException if the private key bytes are not available.
*/ */
public BigInteger getPrivKey() { public BigInteger getPrivKey() {
if (priv == null) if (priv == null) {
throw new MissingPrivateKeyException(); throw new MissingPrivateKeyException();
}
return priv; return priv;
} }
@ -447,8 +485,35 @@ public class ECKey {
* usually encoded using ASN.1 format, so you want {@link ECKey.ECDSASignature#toASN1()} * usually encoded using ASN.1 format, so you want {@link ECKey.ECDSASignature#toASN1()}
* instead. However sometimes the independent components can be useful, for instance, if you're going to do * instead. However sometimes the independent components can be useful, for instance, if you're going to do
* further EC maths on them. * further EC maths on them.
* @throws KeyCrypterException if this ECKey doesn't have a private part.
*/ */
public ECDSASignature sign(Sha256Hash input) { public ECDSASignature sign(Sha256Hash input) throws KeyCrypterException {
return sign(input, null);
}
/**
* Signs the given hash and returns the R and S components as BigIntegers. In the Bitcoin protocol, they are
* usually encoded using DER format, so you want {@link ECKey.ECDSASignature#encodeToDER()}
* instead. However sometimes the independent components can be useful, for instance, if you're doing to do further
* EC maths on them.
*
* @param aesKey The AES key to use for decryption of the private key. If null then no decryption is required.
* @throws KeyCrypterException if there's something wrong with aesKey.
* @throws ECKey.MissingPrivateKeyException if this key cannot sign because it's pubkey only.
*/
public ECDSASignature sign(Sha256Hash input, KeyParameter aesKey) throws KeyCrypterException {
KeyCrypter crypter = getKeyCrypter();
if (crypter != null) {
if (aesKey == null) {
throw new KeyIsEncryptedException();
}
return decrypt(aesKey).sign(input);
} else {
// No decryption of private key required.
if (priv == null) {
throw new MissingPrivateKeyException();
}
}
return doSign(input, priv); return doSign(input, priv);
} }
@ -518,6 +583,29 @@ public class ECKey {
return ECKey.verify(sigHash.getBytes(), signature, getPubKey()); return ECKey.verify(sigHash.getBytes(), signature, getPubKey());
} }
/**
* Verifies the given ASN.1 encoded ECDSA signature against a hash using the public key, and throws an exception
* if the signature doesn't match
* @throws SignatureDecodeException if the signature is unparseable in some way.
* @throws java.security.SignatureException if the signature does not match.
*/
public void verifyOrThrow(byte[] hash, byte[] signature) throws SignatureDecodeException, SignatureException {
if (!verify(hash, signature)) {
throw new SignatureException();
}
}
/**
* Verifies the given R/S pair (signature) against a hash using the public key, and throws an exception
* if the signature doesn't match
* @throws java.security.SignatureException if the signature does not match.
*/
public void verifyOrThrow(Sha256Hash sigHash, ECDSASignature signature) throws SignatureException {
if (!ECKey.verify(sigHash.getBytes(), signature, getPubKey())) {
throw new SignatureException();
}
}
/** /**
* Returns true if the given pubkey is canonical, i.e. the correct length taking into account compression. * Returns true if the given pubkey is canonical, i.e. the correct length taking into account compression.
*/ */
@ -616,6 +704,184 @@ public class ECKey {
return Utils.bigIntegerToBytes(getPrivKey(), 32); return Utils.bigIntegerToBytes(getPrivKey(), 32);
} }
/**
* Returns the creation time of this key or zero if the key was deserialized from a version that did not store
* that data.
*/
@Override
public long getCreationTimeSeconds() {
return creationTimeSeconds;
}
/**
* Sets the creation time of this key. Zero is a convention to mean "unavailable". This method can be useful when
* you have a raw key you are importing from somewhere else.
*/
public void setCreationTimeSeconds(long newCreationTimeSeconds) {
if (newCreationTimeSeconds < 0) {
throw new IllegalArgumentException("Cannot set creation time to negative value: " + newCreationTimeSeconds);
}
creationTimeSeconds = newCreationTimeSeconds;
}
/**
* Create an encrypted private key with the keyCrypter and the AES key supplied.
* This method returns a new encrypted key and leaves the original unchanged.
*
* @param keyCrypter The keyCrypter that specifies exactly how the encrypted bytes are created.
* @param aesKey The KeyParameter with the AES encryption key (usually constructed with keyCrypter#deriveKey and cached as it is slow to create).
* @return encryptedKey
*/
public ECKey encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) throws KeyCrypterException {
if(keyCrypter == null) {
throw new KeyCrypterException("Keycrypter cannot be null");
}
final byte[] privKeyBytes = getPrivKeyBytes();
EncryptedData encryptedPrivateKey = keyCrypter.encrypt(privKeyBytes, null, aesKey);
ECKey result = ECKey.fromEncrypted(encryptedPrivateKey, keyCrypter, getPubKey());
result.setCreationTimeSeconds(creationTimeSeconds);
return result;
}
/**
* Create a decrypted private key with the keyCrypter and AES key supplied. Note that if the aesKey is wrong, this
* has some chance of throwing KeyCrypterException due to the corrupted padding that will result, but it can also
* just yield a garbage key.
*
* @param keyCrypter The keyCrypter that specifies exactly how the decrypted bytes are created.
* @param aesKey The KeyParameter with the AES encryption key (usually constructed with keyCrypter#deriveKey and cached).
*/
public ECKey decrypt(KeyCrypter keyCrypter, KeyParameter aesKey) throws KeyCrypterException {
if(keyCrypter == null) {
throw new KeyCrypterException("Keycrypter cannot be null");
}
// Check that the keyCrypter matches the one used to encrypt the keys, if set.
if (this.keyCrypter != null && !this.keyCrypter.equals(keyCrypter)) {
throw new KeyCrypterException("The keyCrypter being used to decrypt the key is different to the one that was used to encrypt it");
}
if(encryptedPrivateKey == null) {
throw new IllegalArgumentException("This key is not encrypted");
}
byte[] unencryptedPrivateKey = keyCrypter.decrypt(encryptedPrivateKey, aesKey);
ECKey key = ECKey.fromPrivate(unencryptedPrivateKey);
if (!Arrays.equals(key.getPubKey(), getPubKey())) {
throw new KeyCrypterException("Provided AES key is wrong");
}
key.setCreationTimeSeconds(creationTimeSeconds);
return key;
}
/**
* Create a decrypted private key with AES key. Note that if the AES key is wrong, this
* has some chance of throwing KeyCrypterException due to the corrupted padding that will result, but it can also
* just yield a garbage key.
*
* @param aesKey The KeyParameter with the AES encryption key (usually constructed with keyCrypter#deriveKey and cached).
*/
public ECKey decrypt(KeyParameter aesKey) throws KeyCrypterException {
final KeyCrypter crypter = getKeyCrypter();
if (crypter == null) {
throw new KeyCrypterException("No key crypter available");
}
return decrypt(crypter, aesKey);
}
/**
* Creates decrypted private key if needed.
*/
public ECKey maybeDecrypt(KeyParameter aesKey) throws KeyCrypterException {
return isEncrypted() && aesKey != null ? decrypt(aesKey) : this;
}
/**
* <p>Check that it is possible to decrypt the key with the keyCrypter and that the original key is returned.</p>
*
* <p>Because it is a critical failure if the private keys cannot be decrypted successfully (resulting of loss of all
* bitcoins controlled by the private key) you can use this method to check when you *encrypt* a wallet that
* it can definitely be decrypted successfully.</p>
*
* @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) {
try {
ECKey rebornUnencryptedKey = encryptedKey.decrypt(keyCrypter, aesKey);
byte[] originalPrivateKeyBytes = originalKey.getPrivKeyBytes();
byte[] rebornKeyBytes = rebornUnencryptedKey.getPrivKeyBytes();
if (!Arrays.equals(originalPrivateKeyBytes, rebornKeyBytes)) {
log.error("The check that encryption could be reversed failed for {}", originalKey);
return false;
}
return true;
} catch (KeyCrypterException kce) {
log.error(kce.getMessage());
return false;
}
}
/**
* Indicates whether the private key is encrypted (true) or not (false).
* A private key is deemed to be encrypted when there is both a KeyCrypter and the encryptedPrivateKey is non-zero.
*/
@Override
public boolean isEncrypted() {
return keyCrypter != null && encryptedPrivateKey != null && encryptedPrivateKey.getEncryptedBytes().length > 0;
}
@Override
public EncryptionType getEncryptionType() {
return keyCrypter != null ? keyCrypter.getUnderstoodEncryptionType() : EncryptionType.UNENCRYPTED;
}
/**
* A wrapper for {@link #getPrivKeyBytes()} that returns null if the private key bytes are missing or would have
* to be derived (for the HD key case).
*/
@Override
public byte[] getSecretBytes() {
if (hasPrivKey()) {
return getPrivKeyBytes();
} else {
return null;
}
}
/** An alias for {@link #getEncryptedPrivateKey()} */
@Override
public EncryptedData getEncryptedData() {
return getEncryptedPrivateKey();
}
/**
* Returns the the encrypted private key bytes and initialisation vector for this ECKey, or null if the ECKey
* is not encrypted.
*/
public EncryptedData getEncryptedPrivateKey() {
return encryptedPrivateKey;
}
/**
* Returns the KeyCrypter that was used to encrypt to encrypt this ECKey. You need this to decrypt the ECKey.
*/
public KeyCrypter getKeyCrypter() {
return keyCrypter;
}
public static class MissingPrivateKeyException extends RuntimeException {
}
public static class KeyIsEncryptedException extends MissingPrivateKeyException {
}
public static class InvalidPasswordException extends RuntimeException {
}
public static ECKey createKeyPbkdf2HmacSha512(String password) { public static ECKey createKeyPbkdf2HmacSha512(String password) {
return createKeyPbkdf2HmacSha512(password, new byte[0], 1024); return createKeyPbkdf2HmacSha512(password, new byte[0], 1024);
} }
@ -637,24 +903,13 @@ public class ECKey {
byte[] key_m = new byte[hash.length-32]; byte[] key_m = new byte[hash.length-32];
System.arraycopy(hash, 32, key_m, 0, hash.length-32); System.arraycopy(hash, 32, key_m, 0, hash.length-32);
byte[] ciphertext = encryptAesCbcPkcs7(message, iv, key_e); AESKeyCrypter aesKeyCrypter = new AESKeyCrypter();
byte[] ciphertext = aesKeyCrypter.encrypt(message, iv, new KeyParameter(key_e)).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));
} }
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 RuntimeException(e);
}
}
public byte[] decryptEcies(byte[] message, byte[] magic) { public byte[] decryptEcies(byte[] message, byte[] magic) {
byte[] decoded = Base64.getDecoder().decode(message); byte[] decoded = Base64.getDecoder().decode(message);
if(decoded.length < 85) { if(decoded.length < 85) {
@ -691,19 +946,8 @@ public class ECKey {
throw new InvalidPasswordException(); throw new InvalidPasswordException();
} }
return decryptAesCbcPkcs7(ciphertext, iv, key_e); AESKeyCrypter aesKeyCrypter = new AESKeyCrypter();
} return aesKeyCrypter.decrypt(new EncryptedData(iv, ciphertext), new KeyParameter(key_e));
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 RuntimeException(e);
}
} }
private byte[] sha512(byte[] input) { private byte[] sha512(byte[] input) {
@ -753,10 +997,4 @@ public class ECKey {
public String toString() { public String toString() {
return pub.toString(); return pub.toString();
} }
public static class MissingPrivateKeyException extends RuntimeException {
}
public static class InvalidPasswordException extends RuntimeException {
}
} }

View file

@ -0,0 +1,23 @@
package com.sparrowwallet.drongo.crypto;
/**
* Provides a uniform way to access something that can be optionally encrypted with a
* {@link KeyCrypter}, yielding an {@link EncryptedData}, and
* which can have a creation time associated with it.
*/
public interface EncryptableItem {
/** Returns whether the item is encrypted or not. If it is, then {@link #getSecretBytes()} will return null. */
boolean isEncrypted();
/** Returns the raw bytes of the item, if not encrypted, or null if encrypted or the secret is missing. */
byte[] getSecretBytes();
/** Returns the initialization vector and encrypted secret bytes, or null if not encrypted. */
EncryptedData getEncryptedData();
/** Returns an enum constant describing what algorithm was used to encrypt the key or UNENCRYPTED. */
EncryptionType getEncryptionType();
/** Returns the time in seconds since the UNIX epoch at which this encryptable item was first created/derived. */
long getCreationTimeSeconds();
}

View file

@ -0,0 +1,48 @@
package com.sparrowwallet.drongo.crypto;
import java.util.Arrays;
import java.util.Objects;
/**
* <p>An instance of EncryptedData is a holder for an initialization vector and encrypted bytes. It is typically
* used to hold encrypted private key bytes.</p>
*
* <p>The initialisation vector is random data that is used to initialise the AES block cipher when the
* private key bytes were encrypted. You need these for decryption.</p>
*/
public final class EncryptedData {
private final byte[] initialisationVector;
private final byte[] encryptedBytes;
public EncryptedData(byte[] initialisationVector, byte[] encryptedBytes) {
this.initialisationVector = Arrays.copyOf(initialisationVector, initialisationVector.length);
this.encryptedBytes = Arrays.copyOf(encryptedBytes, encryptedBytes.length);
}
public byte[] getInitialisationVector() {
return initialisationVector;
}
public byte[] getEncryptedBytes() {
return encryptedBytes;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EncryptedData other = (EncryptedData) o;
return Arrays.equals(encryptedBytes, other.encryptedBytes) && Arrays.equals(initialisationVector, other.initialisationVector);
}
@Override
public int hashCode() {
return Objects.hash(Arrays.hashCode(encryptedBytes), Arrays.hashCode(initialisationVector));
}
@Override
public String toString() {
return "EncryptedData [initialisationVector=" + Arrays.toString(initialisationVector)
+ ", encryptedPrivateKey=" + Arrays.toString(encryptedBytes) + "]";
}
}

View file

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

View file

@ -0,0 +1,50 @@
package com.sparrowwallet.drongo.crypto;
import org.bouncycastle.crypto.params.KeyParameter;
import java.io.Serializable;
/**
* <p>A KeyCrypter can be used to encrypt and decrypt a message. The sequence of events to encrypt and then decrypt
* a message are as follows:</p>
*
* <p>(1) Ask the user for a password. deriveKey() is then called to create an KeyParameter. This contains the AES
* 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
* 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>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>
*/
public interface KeyCrypter {
/**
* 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
*/
KeyParameter deriveKey(CharSequence password) throws KeyCrypterException;
/**
* Decrypt the provided encrypted bytes, converting them into unencrypted bytes.
*
* @throws KeyCrypterException if decryption was unsuccessful.
*/
byte[] decrypt(EncryptedData encryptedBytesToDecode, KeyParameter aesKey) 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, KeyParameter aesKey) throws KeyCrypterException;
}

View file

@ -0,0 +1,48 @@
package com.sparrowwallet.drongo.crypto;
/**
* <p>Exception to provide the following:</p>
* <ul>
* <li>Provision of encryption / decryption exception</li>
* </ul>
* <p>This base exception acts as a general failure mode not attributable to a specific cause (other than
* that reported in the exception message). Since this is in English, it may not be worth reporting directly
* to the user other than as part of a "general failure to parse" response.</p>
*/
public class KeyCrypterException extends RuntimeException {
public KeyCrypterException(String s) {
super(s);
}
public KeyCrypterException(String s, Throwable throwable) {
super(s, throwable);
}
/**
* This exception is thrown when a private key or seed is decrypted, it doesn't match its public key any
* more. This likely means the wrong decryption key has been used.
*/
public static class PublicPrivateMismatch extends KeyCrypterException {
public PublicPrivateMismatch(String message) {
super(message);
}
public PublicPrivateMismatch(String message, Throwable throwable) {
super(message, throwable);
}
}
/**
* This exception is thrown when a private key or seed is decrypted, the decrypted message is damaged
* (e.g. the padding is damaged). This likely means the wrong decryption key has been used.
*/
public static class InvalidCipherText extends KeyCrypterException {
public InvalidCipherText(String message) {
super(message);
}
public InvalidCipherText(String message, Throwable throwable) {
super(message, throwable);
}
}
}

View file

@ -0,0 +1,271 @@
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;
/**
* <p>This class encrypts and decrypts byte arrays and strings using scrypt as the
* key derivation function and AES for the encryption.</p>
*
* <p>You can use this class to:</p>
*
* <p>1) Using a user password, create an AES key that can encrypt and decrypt your private keys.
* To convert the password to the AES key, scrypt is used. This is an algorithm resistant
* to brute force attacks. You can use the ScryptParameters to tune how difficult you
* want this to be generation to be.</p>
*
* <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>
*/
public class ScryptKeyCrypter implements KeyCrypter {
private static final Logger log = LoggerFactory.getLogger(ScryptKeyCrypter.class);
/**
* Key length in bytes.
*/
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.
*/
public static final int SALT_LENGTH = 8;
private static final SecureRandom secureRandom = new SecureRandom();
/** Returns SALT_LENGTH (8) bytes of random data */
public static byte[] randomSalt() {
byte[] salt = new byte[SALT_LENGTH];
secureRandom.nextBytes(salt);
return salt;
}
// Scrypt parameters.
private final ScryptParameters scryptParameters;
/**
* Encryption/Decryption using default parameters and a random salt.
*/
public ScryptKeyCrypter() {
this.scryptParameters = new ScryptParameters(randomSalt());
}
/**
* 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).
*
* @param iterations
* number of scrypt iterations
*/
public ScryptKeyCrypter(int iterations) {
this.scryptParameters = new ScryptParameters(randomSalt(), iterations);
}
/**
* Encryption/ Decryption using specified Scrypt parameters.
*
* @param scryptParameters ScryptParameters to use
* @throws NullPointerException if the scryptParameters or any of its N, R or P is null.
*/
public ScryptKeyCrypter(ScryptParameters scryptParameters) {
this.scryptParameters = scryptParameters;
if (scryptParameters.getSalt() == null || scryptParameters.getSalt() == null || scryptParameters.getSalt().length == 0) {
log.warn("You are using a ScryptParameters with no salt. Your encryption may be vulnerable to a dictionary attack.");
}
}
/**
* Generate AES key.
*
* This is a very slow operation compared to encrypt/ decrypt so it is normally worth caching the result.
*
* @param password The password to use in key generation
* @return The KeyParameter containing the created AES key
* @throws KeyCrypterException
*/
@Override
public KeyParameter deriveKey(CharSequence password) throws KeyCrypterException {
byte[] passwordBytes = null;
try {
passwordBytes = convertToByteArray(password);
byte[] salt = new byte[0];
if (scryptParameters.getSalt() != null) {
salt = scryptParameters.getSalt();
} else {
log.warn("You are using a ScryptParameters with no salt. Your encryption may be vulnerable to a dictionary attack.");
}
byte[] keyBytes = SCrypt.generate(passwordBytes, salt, (int) scryptParameters.getN(), scryptParameters.getR(), scryptParameters.getP(), KEY_LENGTH);
return new KeyParameter(keyBytes);
} catch (Exception e) {
throw new KeyCrypterException("Could not generate key from password and salt.", e);
} finally {
// Zero the password bytes.
if(passwordBytes != null) {
java.util.Arrays.fill(passwordBytes, (byte) 0);
}
}
}
/**
* 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.
*
* 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;
}
/**
* Return the EncryptionType enum value which denotes the type of encryption/ decryption that this KeyCrypter
* can understand.
*/
@Override
public EncryptionType getUnderstoodEncryptionType() {
return EncryptionType.ENCRYPTED_SCRYPT_AES;
}
@Override
public String toString() {
return "AES-" + KEY_LENGTH * 8 + "-CBC, Scrypt (" + scryptParametersString() + ")";
}
private String scryptParametersString() {
return "N=" + scryptParameters.getN() + ", r=" + scryptParameters.getR() + ", p=" + scryptParameters.getP();
}
@Override
public int hashCode() {
return Objects.hash(scryptParameters);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
return Objects.equals(scryptParameters, ((ScryptKeyCrypter)o).scryptParameters);
}
public static class ScryptParameters {
private final byte[] salt;
private long n = 16384L;
public ScryptParameters(byte[] salt) {
this.salt = salt;
}
public ScryptParameters(byte[] salt, long iterations) {
this.salt = salt;
this.n = iterations;
}
byte[] getSalt() {
return salt;
}
long getN() {
return n;
}
int getR() {
return 8;
}
int getP() {
return 1;
}
}
}

View file

@ -1,108 +0,0 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.text.Normalizer;
import java.util.*;
public class Bip39Calculator {
private static Map<String, Integer> wordlistIndex;
public byte[] getSeed(List<String> mnemonicWords, String passphrase) {
loadWordlistIndex();
int concatLength = mnemonicWords.size() * 11;
StringBuilder concat = new StringBuilder();
for(String mnemonicWord : mnemonicWords) {
Integer index = wordlistIndex.get(mnemonicWord);
if (index == null) {
throw new IllegalArgumentException("Provided mnemonic word \"" + mnemonicWord + "\" is not in the BIP39 english word list");
}
String binaryIndex = addLeadingZeros(Integer.toBinaryString(index), 11);
concat.append(binaryIndex, 0, 11);
}
int checksumLength = concatLength / 33;
int entropyLength = concatLength - checksumLength;
byte[] entropy = byteArrayFromBinaryString(concat.substring(0, entropyLength));
String providedChecksum = concat.substring(entropyLength);
byte[] sha256 = Sha256Hash.hash(entropy);
String calculatedChecksum = addLeadingZeros(Integer.toBinaryString(Byte.toUnsignedInt(sha256[0])), 8).substring(0, checksumLength);
if(!providedChecksum.equals(calculatedChecksum)) {
throw new IllegalArgumentException("Provided mnemonic words do not represent a valid BIP39 seed: checksum failed");
}
String saltStr = "mnemonic";
if(passphrase != null) {
saltStr += Normalizer.normalize(passphrase, Normalizer.Form.NFKD);
}
byte[] salt = saltStr.getBytes(StandardCharsets.UTF_8);
String mnemonic = String.join(" ", mnemonicWords);
mnemonic = Normalizer.normalize(mnemonic, Normalizer.Form.NFKD);
return Utils.getPbkdf2HmacSha512Hash(mnemonic.getBytes(StandardCharsets.UTF_8), salt, 2048);
}
public String addLeadingZeros(String s, int length) {
if (s.length() >= length) return s;
else return String.format("%0" + (length-s.length()) + "d%s", 0, s);
}
private byte[] byteArrayFromBinaryString(String binaryString) {
int splitSize = 8;
if(binaryString.length() < splitSize) {
binaryString = addLeadingZeros(binaryString, 8);
}
if(binaryString.length() % splitSize == 0){
int index = 0;
int position = 0;
byte[] resultByteArray = new byte[binaryString.length()/splitSize];
StringBuilder text = new StringBuilder(binaryString);
while (index < text.length()) {
String binaryStringChunk = text.substring(index, Math.min(index + splitSize, text.length()));
int byteAsInt = Integer.parseInt(binaryStringChunk, 2);
resultByteArray[position] = (byte)byteAsInt;
index += splitSize;
position ++;
}
return resultByteArray;
}
else {
throw new IllegalArgumentException("Cannot convert binary string to byte[], because of the input length '" + binaryString + "' % 8 != 0");
}
}
private synchronized void loadWordlistIndex() {
if(wordlistIndex == null) {
wordlistIndex = new HashMap<>();
try{
BufferedReader reader = new BufferedReader(new InputStreamReader(this.getClass().getResourceAsStream("/wordlist/bip39-english.txt"), StandardCharsets.UTF_8));
String line;
for(int i = 0; (line = reader.readLine()) != null; i++) {
wordlistIndex.put(line.trim(), i);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public List<String> getWordList() {
loadWordlistIndex();
return Collections.unmodifiableList(new ArrayList<>(wordlistIndex.keySet()));
}
}

View file

@ -0,0 +1,230 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Bip39MnemonicCode {
private static final Logger log = LoggerFactory.getLogger(Bip39MnemonicCode.class);
private final ArrayList<String> wordList;
private static final String BIP39_ENGLISH_RESOURCE_NAME = "/wordlist/bip39-english.txt";
private static final String BIP39_ENGLISH_SHA256 = "ad90bf3beb7b0eb7e5acd74727dc0da96e0a280a258354e7293fb7e211ac03db";
/** UNIX time for when the BIP39 standard was finalised. This can be used as a default seed birthday. */
public static long BIP39_STANDARDISATION_TIME_SECS = 1381276800;
private static final int PBKDF2_ROUNDS = 2048;
public static Bip39MnemonicCode INSTANCE;
static {
try {
INSTANCE = new Bip39MnemonicCode();
} catch (RuntimeException e) {
log.error("Failed to load word list", e);
}
}
/** Initialise from the included word list. Won't work on Android. */
public Bip39MnemonicCode() {
this(openDefaultWords(), BIP39_ENGLISH_SHA256);
}
private static InputStream openDefaultWords() {
InputStream stream = Bip39MnemonicCode.class.getResourceAsStream(BIP39_ENGLISH_RESOURCE_NAME);
if(stream == null) {
throw new RuntimeException(new FileNotFoundException(BIP39_ENGLISH_RESOURCE_NAME));
}
return stream;
}
/**
* Creates an MnemonicCode object, initializing with words read from the supplied input stream. If a wordListDigest
* is supplied the digest of the words will be checked.
*/
public Bip39MnemonicCode(InputStream wordstream, String wordListDigest) throws IllegalArgumentException {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(wordstream, StandardCharsets.UTF_8));
this.wordList = new ArrayList<>(2048);
MessageDigest md = Sha256Hash.newDigest();
String word;
while ((word = br.readLine()) != null) {
md.update(word.getBytes());
this.wordList.add(word);
}
br.close();
if (this.wordList.size() != 2048) {
throw new IllegalArgumentException("Input stream did not contain 2048 words");
}
// If a wordListDigest is supplied check to make sure it matches.
if (wordListDigest != null) {
byte[] digest = md.digest();
String hexdigest = Utils.bytesToHex(digest);
if (!hexdigest.equals(wordListDigest)) {
throw new IllegalArgumentException("Wordlist digest mismatch");
}
}
} catch (IOException e) {
throw new RuntimeException("Error loading word list", e);
}
}
/**
* Gets the word list this code uses.
*/
public List<String> getWordList() {
return Collections.unmodifiableList(wordList);
}
/**
* Convert mnemonic word list to seed.
*/
public static byte[] toSeed(List<String> words, String passphrase) {
if(passphrase == null) {
throw new IllegalArgumentException("A null passphrase is not allowed.");
}
// To create binary seed from mnemonic, we use PBKDF2 function
// with mnemonic sentence (in UTF-8) used as a password and
// string "mnemonic" + passphrase (again in UTF-8) used as a
// salt. Iteration count is set to 2048 and HMAC-SHA512 is
// used as a pseudo-random function. Desired length of the
// derived key is 512 bits (= 64 bytes).
//
String mnemonic = String.join(" ", words);
String salt = "mnemonic" + Normalizer.normalize(passphrase, Normalizer.Form.NFKD);
return Utils.getPbkdf2HmacSha512Hash(mnemonic.getBytes(StandardCharsets.UTF_8), salt.getBytes(StandardCharsets.UTF_8), PBKDF2_ROUNDS);
}
/**
* Convert mnemonic word list to original entropy value.
*/
public byte[] toEntropy(List<String> words) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException, MnemonicException.MnemonicChecksumException {
if (words.size() % 3 > 0) {
throw new MnemonicException.MnemonicLengthException("Word list size must be multiple of three words.");
}
if (words.size() == 0) {
throw new MnemonicException.MnemonicLengthException("Word list is empty.");
}
// Look up all the words in the list and construct the
// concatenation of the original entropy and the checksum.
//
int concatLenBits = words.size() * 11;
boolean[] concatBits = new boolean[concatLenBits];
int wordindex = 0;
for (String word : words) {
// Find the words index in the wordlist.
int ndx = Collections.binarySearch(this.wordList, word);
if (ndx < 0) {
throw new MnemonicException.MnemonicWordException(word);
}
// Set the next 11 bits to the value of the index.
for (int ii = 0; ii < 11; ++ii) {
concatBits[(wordindex * 11) + ii] = (ndx & (1 << (10 - ii))) != 0;
}
++wordindex;
}
int checksumLengthBits = concatLenBits / 33;
int entropyLengthBits = concatLenBits - checksumLengthBits;
// Extract original entropy as bytes.
byte[] entropy = new byte[entropyLengthBits / 8];
for (int ii = 0; ii < entropy.length; ++ii) {
for (int jj = 0; jj < 8; ++jj) {
if (concatBits[(ii * 8) + jj]) {
entropy[ii] |= 1 << (7 - jj);
}
}
}
// Take the digest of the entropy.
byte[] hash = Sha256Hash.hash(entropy);
boolean[] hashBits = bytesToBits(hash);
// Check all the checksum bits.
for (int i = 0; i < checksumLengthBits; ++i) {
if (concatBits[entropyLengthBits + i] != hashBits[i]) {
throw new MnemonicException.MnemonicChecksumException();
}
}
return entropy;
}
/**
* Convert entropy data to mnemonic word list.
*/
public List<String> toMnemonic(byte[] entropy) throws MnemonicException.MnemonicLengthException {
if (entropy.length % 4 > 0) {
throw new MnemonicException.MnemonicLengthException("Entropy length not multiple of 32 bits.");
}
if (entropy.length == 0) {
throw new MnemonicException.MnemonicLengthException("Entropy is empty.");
}
// We take initial entropy of ENT bits and compute its
// checksum by taking first ENT / 32 bits of its SHA256 hash.
byte[] hash = Sha256Hash.hash(entropy);
boolean[] hashBits = bytesToBits(hash);
boolean[] entropyBits = bytesToBits(entropy);
int checksumLengthBits = entropyBits.length / 32;
// We append these bits to the end of the initial entropy.
boolean[] concatBits = new boolean[entropyBits.length + checksumLengthBits];
System.arraycopy(entropyBits, 0, concatBits, 0, entropyBits.length);
System.arraycopy(hashBits, 0, concatBits, entropyBits.length, checksumLengthBits);
// Next we take these concatenated bits and split them into
// groups of 11 bits. Each group encodes number from 0-2047
// which is a position in a wordlist. We convert numbers into
// words and use joined words as mnemonic sentence.
ArrayList<String> words = new ArrayList<>();
int nwords = concatBits.length / 11;
for (int i = 0; i < nwords; ++i) {
int index = 0;
for (int j = 0; j < 11; ++j) {
index <<= 1;
if (concatBits[(i * 11) + j]) {
index |= 0x1;
}
}
words.add(this.wordList.get(index));
}
return words;
}
/**
* Check to see if a mnemonic word list is valid.
*/
public void check(List<String> words) throws MnemonicException {
toEntropy(words);
}
private static boolean[] bytesToBits(byte[] data) {
boolean[] bits = new boolean[data.length * 8];
for (int i = 0; i < data.length; ++i)
for (int j = 0; j < 8; ++j)
bits[(i * 8) + j] = (data[i] & (1 << (7 - j))) != 0;
return bits;
}
}

View file

@ -0,0 +1,164 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.EncryptableItem;
import com.sparrowwallet.drongo.crypto.EncryptedData;
import com.sparrowwallet.drongo.crypto.EncryptionType;
import com.sparrowwallet.drongo.crypto.KeyCrypter;
import org.bouncycastle.crypto.params.KeyParameter;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
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 long creationTimeSeconds;
public DeterministicSeed(byte[] seed, long creationTimeSeconds) {
this.seed = seed;
this.encryptedSeed = null;
this.creationTimeSeconds = creationTimeSeconds;
}
public DeterministicSeed(EncryptedData encryptedSeed, long creationTimeSeconds) {
this.seed = null;
this.encryptedSeed = encryptedSeed;
this.creationTimeSeconds = creationTimeSeconds;
}
/**
* Constructs a seed from a BIP 39 mnemonic code. See {@link Bip39MnemonicCode} for more
* details on this scheme.
* @param random Entropy source
* @param bits number of bits, must be divisible by 32
* @param passphrase A user supplied passphrase, or an empty string if there is no passphrase
*/
public DeterministicSeed(SecureRandom random, int bits, String passphrase) {
this(getEntropy(random, bits), passphrase, System.currentTimeMillis());
}
/**
* Constructs a seed from a BIP 39 mnemonic code. See {@link Bip39MnemonicCode} for more
* details on this scheme.
* @param entropy entropy bits, length must be divisible by 32
* @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(byte[] entropy, String passphrase, long creationTimeSeconds) {
if(entropy.length % 4 != 0) {
throw new IllegalArgumentException("Entropy size in bits not divisible by 32");
}
if(entropy.length * 8 < DEFAULT_SEED_ENTROPY_BITS) {
throw new IllegalArgumentException("Entropy size too small");
}
if(passphrase == null) {
passphrase = "";
}
List<String> mnemonicCode;
try {
mnemonicCode = Bip39MnemonicCode.INSTANCE.toMnemonic(entropy);
} catch (MnemonicException.MnemonicLengthException e) {
// cannot happen
throw new RuntimeException(e);
}
this.seed = Bip39MnemonicCode.toSeed(mnemonicCode, passphrase);
this.encryptedSeed = null;
this.creationTimeSeconds = creationTimeSeconds;
}
private static byte[] getEntropy(SecureRandom random, int bits) {
if(bits > MAX_SEED_ENTROPY_BITS) {
throw new IllegalArgumentException("Requested entropy size too large");
}
byte[] seed = new byte[bits / 8];
random.nextBytes(seed);
return seed;
}
@Override
public boolean isEncrypted() {
return encryptedSeed != null;
}
public String toString() {
if(isEncrypted()) {
return encryptedSeed.toString();
}
return Utils.bytesToHex(seed);
}
/** Returns the seed as hex or null if encrypted. */
public String toHexString() {
return seed != null ? Utils.bytesToHex(seed) : null;
}
@Override
public byte[] getSecretBytes() {
return getSeedBytes();
}
public byte[] getSeedBytes() {
return seed;
}
@Override
public EncryptedData getEncryptedData() {
return encryptedSeed;
}
@Override
public EncryptionType getEncryptionType() {
return EncryptionType.ENCRYPTED_SCRYPT_AES;
}
@Override
public long getCreationTimeSeconds() {
return creationTimeSeconds;
}
public void setCreationTimeSeconds(long creationTimeSeconds) {
this.creationTimeSeconds = creationTimeSeconds;
}
public DeterministicSeed encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) {
if(encryptedSeed != null) {
throw new IllegalArgumentException("Trying to encrypt seed twice");
}
EncryptedData encryptedSeed = keyCrypter.encrypt(seed, null, aesKey);
return new DeterministicSeed(encryptedSeed, creationTimeSeconds);
}
public DeterministicSeed decrypt(KeyCrypter crypter, String passphrase, KeyParameter aesKey) {
if(!isEncrypted()) {
throw new IllegalStateException("Cannot decrypt unencrypted seed");
}
byte[] seed = crypter.decrypt(encryptedSeed, aesKey);
return new DeterministicSeed(seed, passphrase, creationTimeSeconds);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DeterministicSeed other = (DeterministicSeed) o;
return creationTimeSeconds == other.creationTimeSeconds
&& (isEncrypted() ? encryptedSeed.equals(other.encryptedSeed) : Arrays.equals(seed, other.seed));
}
@Override
public int hashCode() {
return Objects.hash(creationTimeSeconds, isEncrypted() ? encryptedSeed : seed);
}
}

View file

@ -0,0 +1,42 @@
package com.sparrowwallet.drongo.wallet;
public class MnemonicException extends Exception {
public MnemonicException() {
super();
}
public MnemonicException(String msg) {
super(msg);
}
/**
* Thrown when an argument to MnemonicCode is the wrong length.
*/
public static class MnemonicLengthException extends MnemonicException {
public MnemonicLengthException(String msg) {
super(msg);
}
}
/**
* Thrown when a list of MnemonicCode words fails the checksum check.
*/
public static class MnemonicChecksumException extends MnemonicException {
public MnemonicChecksumException() {
super();
}
}
/**
* Thrown when a word is encountered which is not in the MnemonicCode's word list.
*/
public static class MnemonicWordException extends MnemonicException {
/** Contains the word that was not found in the word list. */
public final String badWord;
public MnemonicWordException(String badWord) {
super();
this.badWord = badWord;
}
}
}

View file

@ -7,89 +7,89 @@ import org.junit.Test;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
public class Bip39CalculatorTest { public class Bip39MnemonicCodeTest {
@Test @Test
public void bip39TwelveWordsTest() { public void bip39TwelveWordsTest() throws MnemonicException {
String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor"; String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor";
List<String> wordlist = Arrays.asList(words.split(" ")); List<String> wordlist = Arrays.asList(words.split(" "));
Bip39Calculator bip39Calculator = new Bip39Calculator(); Bip39MnemonicCode.INSTANCE.check(wordlist);
byte[] seed = bip39Calculator.getSeed(wordlist, ""); byte[] seed = Bip39MnemonicCode.toSeed(wordlist, "");
Assert.assertEquals("727ecfcf0bce9d8ec0ef066f7aeb845c271bdd4ee06a37398cebd40dc810140bb620b6c10a8ad671afdceaf37aa55d92d6478f747e8b92430dd938ab5be961dd", Utils.bytesToHex(seed)); Assert.assertEquals("727ecfcf0bce9d8ec0ef066f7aeb845c271bdd4ee06a37398cebd40dc810140bb620b6c10a8ad671afdceaf37aa55d92d6478f747e8b92430dd938ab5be961dd", Utils.bytesToHex(seed));
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = MnemonicException.MnemonicChecksumException.class)
public void bip39TwelveWordsInvalidTest() { public void bip39TwelveWordsInvalidTest() throws MnemonicException {
String words = "absent absent absent absent absent absent absent absent absent absent absent absent"; String words = "absent absent absent absent absent absent absent absent absent absent absent absent";
List<String> wordlist = Arrays.asList(words.split(" ")); List<String> wordlist = Arrays.asList(words.split(" "));
Bip39Calculator bip39Calculator = new Bip39Calculator(); Bip39MnemonicCode.INSTANCE.check(wordlist);
byte[] seed = bip39Calculator.getSeed(wordlist, ""); byte[] seed = Bip39MnemonicCode.toSeed(wordlist, "");
} }
@Test @Test
public void bip39TwelveWordsPassphraseTest() { public void bip39TwelveWordsPassphraseTest() throws MnemonicException {
String words = "arch easily near social civil image seminar monkey engine party promote turtle"; String words = "arch easily near social civil image seminar monkey engine party promote turtle";
List<String> wordlist = Arrays.asList(words.split(" ")); List<String> wordlist = Arrays.asList(words.split(" "));
Bip39Calculator bip39Calculator = new Bip39Calculator(); Bip39MnemonicCode.INSTANCE.check(wordlist);
byte[] seed = bip39Calculator.getSeed(wordlist, "anotherpass867"); byte[] seed = Bip39MnemonicCode.toSeed(wordlist, "anotherpass867");
Assert.assertEquals("ca50764cda44a2cf52aef3c677bebf26011f9dc2b9fddfed2a8a5a9ecb8542956990a16e6873b7724044e83708d9d3a662b765e8800e6e79b289f51c2bcad756", Utils.bytesToHex(seed)); Assert.assertEquals("ca50764cda44a2cf52aef3c677bebf26011f9dc2b9fddfed2a8a5a9ecb8542956990a16e6873b7724044e83708d9d3a662b765e8800e6e79b289f51c2bcad756", Utils.bytesToHex(seed));
} }
@Test @Test
public void bip39FifteenWordsTest() { public void bip39FifteenWordsTest() throws MnemonicException {
String words = "open grunt omit snap behave inch engine hamster hope increase exotic segment news choose roast"; String words = "open grunt omit snap behave inch engine hamster hope increase exotic segment news choose roast";
List<String> wordlist = Arrays.asList(words.split(" ")); List<String> wordlist = Arrays.asList(words.split(" "));
Bip39Calculator bip39Calculator = new Bip39Calculator(); Bip39MnemonicCode.INSTANCE.check(wordlist);
byte[] seed = bip39Calculator.getSeed(wordlist, ""); byte[] seed = Bip39MnemonicCode.toSeed(wordlist, "");
Assert.assertEquals("2174deae5fd315253dc065db7ef97f46957eb68a12505adccfb7f8aca5b63788c587e73430848f85417d9a7d95e6396d2eb3af73c9fb507ebcb9268a5ad47885", Utils.bytesToHex(seed)); Assert.assertEquals("2174deae5fd315253dc065db7ef97f46957eb68a12505adccfb7f8aca5b63788c587e73430848f85417d9a7d95e6396d2eb3af73c9fb507ebcb9268a5ad47885", Utils.bytesToHex(seed));
} }
@Test @Test
public void bip39EighteenWordsTest() { public void bip39EighteenWordsTest() throws MnemonicException {
String words = "mandate lend daring actual health dilemma throw muffin garden pony inherit volume slim visual police supreme bless crush"; String words = "mandate lend daring actual health dilemma throw muffin garden pony inherit volume slim visual police supreme bless crush";
List<String> wordlist = Arrays.asList(words.split(" ")); List<String> wordlist = Arrays.asList(words.split(" "));
Bip39Calculator bip39Calculator = new Bip39Calculator(); Bip39MnemonicCode.INSTANCE.check(wordlist);
byte[] seed = bip39Calculator.getSeed(wordlist, ""); byte[] seed = Bip39MnemonicCode.toSeed(wordlist, "");
Assert.assertEquals("04bd65f582e288bbf595213048b06e1552017776d20ca290ac06d840e197bcaaccd4a85a45a41219be4183dd2e521e7a7a2d6aea3069f04e503ef6d9c8dfa651", Utils.bytesToHex(seed)); Assert.assertEquals("04bd65f582e288bbf595213048b06e1552017776d20ca290ac06d840e197bcaaccd4a85a45a41219be4183dd2e521e7a7a2d6aea3069f04e503ef6d9c8dfa651", Utils.bytesToHex(seed));
} }
@Test @Test
public void bip39TwentyOneWordsTest() { public void bip39TwentyOneWordsTest() throws MnemonicException {
String words = "mirror milk file hope drill conduct empty mutual physical easily sell patient green final release excuse name asset update advance resource"; String words = "mirror milk file hope drill conduct empty mutual physical easily sell patient green final release excuse name asset update advance resource";
List<String> wordlist = Arrays.asList(words.split(" ")); List<String> wordlist = Arrays.asList(words.split(" "));
Bip39Calculator bip39Calculator = new Bip39Calculator(); Bip39MnemonicCode.INSTANCE.check(wordlist);
byte[] seed = bip39Calculator.getSeed(wordlist, ""); byte[] seed = Bip39MnemonicCode.toSeed(wordlist, "");
Assert.assertEquals("f3a88a437153333f9759f323dfe7910e6a649c34da5800e6c978d77baad54b67b06eab17c0107243f3e8b395a2de98c910e9528127539efda2eea5ae50e94019", Utils.bytesToHex(seed)); Assert.assertEquals("f3a88a437153333f9759f323dfe7910e6a649c34da5800e6c978d77baad54b67b06eab17c0107243f3e8b395a2de98c910e9528127539efda2eea5ae50e94019", Utils.bytesToHex(seed));
} }
@Test @Test
public void bip39TwentyFourWordsTest() { public void bip39TwentyFourWordsTest() throws MnemonicException {
String words = "earth easily dwarf dance forum muscle brick often huge base long steel silk frost quiz liquid echo adapt annual expand slim rookie venture oval"; String words = "earth easily dwarf dance forum muscle brick often huge base long steel silk frost quiz liquid echo adapt annual expand slim rookie venture oval";
List<String> wordlist = Arrays.asList(words.split(" ")); List<String> wordlist = Arrays.asList(words.split(" "));
Bip39Calculator bip39Calculator = new Bip39Calculator(); Bip39MnemonicCode.INSTANCE.check(wordlist);
byte[] seed = bip39Calculator.getSeed(wordlist, ""); byte[] seed = Bip39MnemonicCode.toSeed(wordlist, "");
Assert.assertEquals("60f825219a1fcfa479de28435e9bf2aa5734e212982daee582ca0427ad6141c65be9863c3ce0f18e2b173083ea49dcf47d07148734a5f748ac60d470cee6a2bc", Utils.bytesToHex(seed)); Assert.assertEquals("60f825219a1fcfa479de28435e9bf2aa5734e212982daee582ca0427ad6141c65be9863c3ce0f18e2b173083ea49dcf47d07148734a5f748ac60d470cee6a2bc", Utils.bytesToHex(seed));
} }
@Test @Test
public void bip39TwentyFourWordsPassphraseTest() { public void bip39TwentyFourWordsPassphraseTest() throws MnemonicException {
String words = "earth easily dwarf dance forum muscle brick often huge base long steel silk frost quiz liquid echo adapt annual expand slim rookie venture oval"; String words = "earth easily dwarf dance forum muscle brick often huge base long steel silk frost quiz liquid echo adapt annual expand slim rookie venture oval";
List<String> wordlist = Arrays.asList(words.split(" ")); List<String> wordlist = Arrays.asList(words.split(" "));
Bip39Calculator bip39Calculator = new Bip39Calculator(); Bip39MnemonicCode.INSTANCE.check(wordlist);
byte[] seed = bip39Calculator.getSeed(wordlist, "thispass"); byte[] seed = Bip39MnemonicCode.toSeed(wordlist, "thispass");
Assert.assertEquals("a652d123f421f56257391af26063e900619678b552dafd3850e699f6da0667269bbcaebb0509557481db29607caac0294b3cd337d740174cfa05f552fe9e0272", Utils.bytesToHex(seed)); Assert.assertEquals("a652d123f421f56257391af26063e900619678b552dafd3850e699f6da0667269bbcaebb0509557481db29607caac0294b3cd337d740174cfa05f552fe9e0272", Utils.bytesToHex(seed));
} }