mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-26 10:06:45 +00:00
encryptableitem and bip39 from bitcoinj
This commit is contained in:
parent
be0c4d1176
commit
dc569979e1
13 changed files with 1252 additions and 174 deletions
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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) + "]";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.sparrowwallet.drongo.crypto;
|
||||||
|
|
||||||
|
public enum EncryptionType {
|
||||||
|
UNENCRYPTED, ENCRYPTED_SCRYPT_AES, ENCRYPTED_ECIES;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
}
|
}
|
Loading…
Reference in a new issue