mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-26 01:56:44 +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.ec.CustomNamedCurves;
|
||||
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
|
||||
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
|
||||
import org.bouncycastle.crypto.macs.HMac;
|
||||
import org.bouncycastle.crypto.params.*;
|
||||
import org.bouncycastle.crypto.signers.ECDSASigner;
|
||||
|
@ -24,17 +23,14 @@ import org.bouncycastle.util.encoders.Hex;
|
|||
import org.slf4j.Logger;
|
||||
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.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.*;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Comparator;
|
||||
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
|
||||
* 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);
|
||||
|
||||
/** 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.
|
||||
private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
|
||||
|
||||
|
@ -101,6 +109,9 @@ public class ECKey {
|
|||
// not have this field.
|
||||
protected long creationTimeSeconds;
|
||||
|
||||
protected KeyCrypter keyCrypter;
|
||||
protected EncryptedData encryptedPrivateKey;
|
||||
|
||||
private byte[] pubKeyHash;
|
||||
|
||||
/**
|
||||
|
@ -145,6 +156,18 @@ public class ECKey {
|
|||
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.
|
||||
* See the ECKey class docs for a discussion of point compression.
|
||||
|
@ -234,6 +257,19 @@ public class ECKey {
|
|||
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
|
||||
* in its wallet storage format.
|
||||
|
@ -313,8 +349,10 @@ public class ECKey {
|
|||
* @throws java.lang.IllegalStateException if the private key bytes are not available.
|
||||
*/
|
||||
public BigInteger getPrivKey() {
|
||||
if (priv == null)
|
||||
if (priv == null) {
|
||||
throw new MissingPrivateKeyException();
|
||||
}
|
||||
|
||||
return priv;
|
||||
}
|
||||
|
||||
|
@ -447,8 +485,35 @@ public class ECKey {
|
|||
* 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
|
||||
* 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);
|
||||
}
|
||||
|
||||
|
@ -518,6 +583,29 @@ public class ECKey {
|
|||
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.
|
||||
*/
|
||||
|
@ -616,6 +704,184 @@ public class ECKey {
|
|||
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) {
|
||||
return createKeyPbkdf2HmacSha512(password, new byte[0], 1024);
|
||||
}
|
||||
|
@ -637,24 +903,13 @@ public class ECKey {
|
|||
byte[] key_m = new byte[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[] result = hmac256(key_m, encrypted);
|
||||
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) {
|
||||
byte[] decoded = Base64.getDecoder().decode(message);
|
||||
if(decoded.length < 85) {
|
||||
|
@ -691,19 +946,8 @@ public class ECKey {
|
|||
throw new InvalidPasswordException();
|
||||
}
|
||||
|
||||
return decryptAesCbcPkcs7(ciphertext, iv, 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);
|
||||
}
|
||||
AESKeyCrypter aesKeyCrypter = new AESKeyCrypter();
|
||||
return aesKeyCrypter.decrypt(new EncryptedData(iv, ciphertext), new KeyParameter(key_e));
|
||||
}
|
||||
|
||||
private byte[] sha512(byte[] input) {
|
||||
|
@ -753,10 +997,4 @@ public class ECKey {
|
|||
public String 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.List;
|
||||
|
||||
public class Bip39CalculatorTest {
|
||||
public class Bip39MnemonicCodeTest {
|
||||
@Test
|
||||
public void bip39TwelveWordsTest() {
|
||||
public void bip39TwelveWordsTest() throws MnemonicException {
|
||||
String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor";
|
||||
List<String> wordlist = Arrays.asList(words.split(" "));
|
||||
|
||||
Bip39Calculator bip39Calculator = new Bip39Calculator();
|
||||
byte[] seed = bip39Calculator.getSeed(wordlist, "");
|
||||
Bip39MnemonicCode.INSTANCE.check(wordlist);
|
||||
byte[] seed = Bip39MnemonicCode.toSeed(wordlist, "");
|
||||
|
||||
Assert.assertEquals("727ecfcf0bce9d8ec0ef066f7aeb845c271bdd4ee06a37398cebd40dc810140bb620b6c10a8ad671afdceaf37aa55d92d6478f747e8b92430dd938ab5be961dd", Utils.bytesToHex(seed));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void bip39TwelveWordsInvalidTest() {
|
||||
@Test(expected = MnemonicException.MnemonicChecksumException.class)
|
||||
public void bip39TwelveWordsInvalidTest() throws MnemonicException {
|
||||
String words = "absent absent absent absent absent absent absent absent absent absent absent absent";
|
||||
List<String> wordlist = Arrays.asList(words.split(" "));
|
||||
|
||||
Bip39Calculator bip39Calculator = new Bip39Calculator();
|
||||
byte[] seed = bip39Calculator.getSeed(wordlist, "");
|
||||
Bip39MnemonicCode.INSTANCE.check(wordlist);
|
||||
byte[] seed = Bip39MnemonicCode.toSeed(wordlist, "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bip39TwelveWordsPassphraseTest() {
|
||||
public void bip39TwelveWordsPassphraseTest() throws MnemonicException {
|
||||
String words = "arch easily near social civil image seminar monkey engine party promote turtle";
|
||||
List<String> wordlist = Arrays.asList(words.split(" "));
|
||||
|
||||
Bip39Calculator bip39Calculator = new Bip39Calculator();
|
||||
byte[] seed = bip39Calculator.getSeed(wordlist, "anotherpass867");
|
||||
Bip39MnemonicCode.INSTANCE.check(wordlist);
|
||||
byte[] seed = Bip39MnemonicCode.toSeed(wordlist, "anotherpass867");
|
||||
|
||||
Assert.assertEquals("ca50764cda44a2cf52aef3c677bebf26011f9dc2b9fddfed2a8a5a9ecb8542956990a16e6873b7724044e83708d9d3a662b765e8800e6e79b289f51c2bcad756", Utils.bytesToHex(seed));
|
||||
}
|
||||
|
||||
@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";
|
||||
List<String> wordlist = Arrays.asList(words.split(" "));
|
||||
|
||||
Bip39Calculator bip39Calculator = new Bip39Calculator();
|
||||
byte[] seed = bip39Calculator.getSeed(wordlist, "");
|
||||
Bip39MnemonicCode.INSTANCE.check(wordlist);
|
||||
byte[] seed = Bip39MnemonicCode.toSeed(wordlist, "");
|
||||
|
||||
Assert.assertEquals("2174deae5fd315253dc065db7ef97f46957eb68a12505adccfb7f8aca5b63788c587e73430848f85417d9a7d95e6396d2eb3af73c9fb507ebcb9268a5ad47885", Utils.bytesToHex(seed));
|
||||
}
|
||||
|
||||
@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";
|
||||
List<String> wordlist = Arrays.asList(words.split(" "));
|
||||
|
||||
Bip39Calculator bip39Calculator = new Bip39Calculator();
|
||||
byte[] seed = bip39Calculator.getSeed(wordlist, "");
|
||||
Bip39MnemonicCode.INSTANCE.check(wordlist);
|
||||
byte[] seed = Bip39MnemonicCode.toSeed(wordlist, "");
|
||||
|
||||
Assert.assertEquals("04bd65f582e288bbf595213048b06e1552017776d20ca290ac06d840e197bcaaccd4a85a45a41219be4183dd2e521e7a7a2d6aea3069f04e503ef6d9c8dfa651", Utils.bytesToHex(seed));
|
||||
}
|
||||
|
||||
@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";
|
||||
List<String> wordlist = Arrays.asList(words.split(" "));
|
||||
|
||||
Bip39Calculator bip39Calculator = new Bip39Calculator();
|
||||
byte[] seed = bip39Calculator.getSeed(wordlist, "");
|
||||
Bip39MnemonicCode.INSTANCE.check(wordlist);
|
||||
byte[] seed = Bip39MnemonicCode.toSeed(wordlist, "");
|
||||
|
||||
Assert.assertEquals("f3a88a437153333f9759f323dfe7910e6a649c34da5800e6c978d77baad54b67b06eab17c0107243f3e8b395a2de98c910e9528127539efda2eea5ae50e94019", Utils.bytesToHex(seed));
|
||||
}
|
||||
|
||||
@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";
|
||||
List<String> wordlist = Arrays.asList(words.split(" "));
|
||||
|
||||
Bip39Calculator bip39Calculator = new Bip39Calculator();
|
||||
byte[] seed = bip39Calculator.getSeed(wordlist, "");
|
||||
Bip39MnemonicCode.INSTANCE.check(wordlist);
|
||||
byte[] seed = Bip39MnemonicCode.toSeed(wordlist, "");
|
||||
|
||||
Assert.assertEquals("60f825219a1fcfa479de28435e9bf2aa5734e212982daee582ca0427ad6141c65be9863c3ce0f18e2b173083ea49dcf47d07148734a5f748ac60d470cee6a2bc", Utils.bytesToHex(seed));
|
||||
}
|
||||
|
||||
@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";
|
||||
List<String> wordlist = Arrays.asList(words.split(" "));
|
||||
|
||||
Bip39Calculator bip39Calculator = new Bip39Calculator();
|
||||
byte[] seed = bip39Calculator.getSeed(wordlist, "thispass");
|
||||
Bip39MnemonicCode.INSTANCE.check(wordlist);
|
||||
byte[] seed = Bip39MnemonicCode.toSeed(wordlist, "thispass");
|
||||
|
||||
Assert.assertEquals("a652d123f421f56257391af26063e900619678b552dafd3850e699f6da0667269bbcaebb0509557481db29607caac0294b3cd337d740174cfa05f552fe9e0272", Utils.bytesToHex(seed));
|
||||
}
|
Loading…
Reference in a new issue