3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-07 06:44:16 +00:00

Merge encrypted wallets functionality by Jim Burton.

This commit is contained in:
Mike Hearn 2013-01-23 17:12:53 +01:00
parent 17efb4d6b7
commit 5d0518dafd
22 changed files with 3702 additions and 260 deletions

View File

@ -164,6 +164,12 @@
<artifactId>jcip-annotations</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.lambdaworks</groupId>
<artifactId>scrypt</artifactId>
<version>1.3.3</version>
</dependency>
</dependencies>
</project>

View File

@ -1,4 +1,4 @@
/** Copyright 2012 Google Inc.
/** Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -34,7 +34,16 @@ message PeerAddress {
}
/**
* A key use to control Bitcoin spending
* The data to store a private key encrypted with Scrypt and AES
*
*/
message EncryptedPrivateKey {
required bytes initialisation_vector = 1; // The initialisation vector for the AES encryption (16 bytes)
required bytes encrypted_private_key = 2; // The encrypted private key
}
/**
* A key used to control Bitcoin spending.
*
* Either the private key, the public key or both may be present. It is recommended that
* if the private key is provided that the public key is provided too because deriving it is slow.
@ -44,12 +53,19 @@ message PeerAddress {
*/
message Key {
enum Type {
ORIGINAL = 1; // Original bitcoin secp256k1 curve
ORIGINAL = 1; // Unencrypted - Original bitcoin secp256k1 curve
ENCRYPTED_SCRYPT_AES = 2; // Encrypted with Scrypt and AES - - Original bitcoin secp256k1 curve
}
required Type type = 1;
// The private EC key bytes without any ASN.1 wrapping.
optional bytes private_key = 2;
// The message containing the encrypted private EC key information.
// When an EncryptedPrivateKey is present then the (unencrypted) private_key will be a zero length byte array or contain all zeroes.
// This is for security of the private key information.
optional EncryptedPrivateKey encrypted_private_key = 6;
// The public EC key derived from the private key. We allow both to be stored to avoid mobile clients having to
// do lots of slow EC math on startup.
optional bytes public_key = 3;
@ -176,6 +192,19 @@ message Transaction {
optional TransactionConfidence confidence = 9;
}
/** The parameters used in the scrypt key derivation function.
* The default values are taken from http://www.tarsnap.com/scrypt/scrypt-slides.pdf.
* They can be increased - n is the number of iterations performed and
* r and p can be used to tweak the algorithm - see:
* http://stackoverflow.com/questions/11126315/what-are-optimal-scrypt-work-factors
*/
message ScryptParameters {
required bytes salt = 1; // Salt to use in generation of the wallet password (8 bytes)
optional int64 n = 2 [default = 16384]; // CPU/ memory cost parameter
optional int32 r = 3 [default = 8]; // Block size parameter
optional int32 p = 4 [default = 1]; // Parallelisation parameter
}
/** An extension to the wallet */
message Extension {
required string id = 1; // like org.whatever.foo.bar
@ -188,6 +217,18 @@ message Extension {
/** A bitcoin wallet */
message Wallet {
/**
* The encryption type of the wallet.
*
* The encryption type is UNENCRYPTED for wallets where the wallet does not support encryption - wallets prior to
* encryption support are grandfathered in as this wallet type.
* When a wallet is ENCRYPTED_SCRYPT_AES the keys are either encrypted with the wallet password or are unencrypted.
*/
enum EncryptionType {
UNENCRYPTED = 1; // All keys in the wallet are unencrypted
ENCRYPTED_SCRYPT_AES = 2; // All keys are encrypted with a passphrase based KDF of scrypt and AES encryption
}
required string network_identifier = 1; // the network used by this wallet
// org.bitcoin.production = production network (Satoshi genesis block)
// org.bitcoin.test = test network (Andresen genesis block)
@ -195,9 +236,25 @@ message Wallet {
// The SHA256 hash of the head of the best chain seen by this wallet.
optional bytes last_seen_block_hash = 2;
// The height in the chain of the last seen block.
optional uint32 last_seen_block_height = 5;
optional uint32 last_seen_block_height = 12;
repeated Key key = 3;
repeated Transaction transaction = 4;
optional EncryptionType encryption_type = 5 [default=UNENCRYPTED];
optional ScryptParameters encryption_parameters = 6;
// The version number of the wallet - used to detect wallets that were produced in the future
// (i.e the wallet may contain some future format this protobuf/ code does not know about)
optional int32 version = 7;
// deprecated - do not recycle this numeric identifier
// optional int32 minor_version = 8;
repeated Extension extension = 10;
// A UTF8 encoded text description of the wallet that is intended for end user provided text.
optional string description = 11;
// (The field number 12 is used by last_seen_block_height)
} // end of Wallet

View File

@ -648,8 +648,8 @@ public abstract class AbstractBlockChain {
}
}
}
private void setChainHead(StoredBlock chainHead) throws BlockStoreException {
protected void setChainHead(StoredBlock chainHead) throws BlockStoreException {
doSetChainHead(chainHead);
synchronized (chainHeadLock) {
this.chainHead = chainHead;

View File

@ -16,6 +16,8 @@
package com.google.bitcoin.core;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Preconditions;
import org.spongycastle.asn1.*;
import org.spongycastle.asn1.sec.SECNamedCurves;
@ -26,12 +28,17 @@ import org.spongycastle.crypto.params.ECDomainParameters;
import org.spongycastle.crypto.params.ECKeyGenerationParameters;
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
import org.spongycastle.crypto.params.ECPublicKeyParameters;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.crypto.signers.ECDSASigner;
import org.spongycastle.math.ec.ECCurve;
import org.spongycastle.math.ec.ECFieldElement;
import org.spongycastle.math.ec.ECPoint;
import org.spongycastle.util.encoders.Base64;
import com.google.bitcoin.crypto.EncryptedPrivateKey;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
@ -61,6 +68,8 @@ import static com.google.common.base.Preconditions.checkArgument;
* signature and want to find out who signed it, rather than requiring the user to provide the expected identity.</p>
*/
public class ECKey implements Serializable {
private static final Logger log = LoggerFactory.getLogger(ECKey.class);
private static final ECDomainParameters ecParams;
private static final SecureRandom secureRandom;
@ -82,6 +91,16 @@ public class ECKey implements Serializable {
// not have this field.
private long creationTimeSeconds;
/**
* Instance of the KeyCrypter interface to use for encrypting and decrypting the key.
*/
transient private KeyCrypter keyCrypter;
/**
* The encrypted private key information.
*/
private EncryptedPrivateKey encryptedPrivateKey;
// Transient because it's calculated on demand.
transient private byte[] pubKeyHash;
@ -118,30 +137,37 @@ public class ECKey implements Serializable {
return new ECKey(extractPrivateKeyFromASN1(asn1privkey));
}
/**
* Output this ECKey as an ASN.1 encoded private key, as understood by OpenSSL or used by the Bitcoin reference
* implementation in its wallet storage format.
*/
public byte[] toASN1() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream(400);
/** Creates an ECKey given the private key only. The public key is calculated from it (this is slow) */
public ECKey(BigInteger privKey) {
this(privKey, (byte[])null);
}
// ASN1_SEQUENCE(EC_PRIVATEKEY) = {
// ASN1_SIMPLE(EC_PRIVATEKEY, version, LONG),
// ASN1_SIMPLE(EC_PRIVATEKEY, privateKey, ASN1_OCTET_STRING),
// ASN1_EXP_OPT(EC_PRIVATEKEY, parameters, ECPKPARAMETERS, 0),
// ASN1_EXP_OPT(EC_PRIVATEKEY, publicKey, ASN1_BIT_STRING, 1)
// } ASN1_SEQUENCE_END(EC_PRIVATEKEY)
DERSequenceGenerator seq = new DERSequenceGenerator(baos);
seq.addObject(new ASN1Integer(1)); // version
seq.addObject(new DEROctetString(priv.toByteArray()));
seq.addObject(new DERTaggedObject(0, SECNamedCurves.getByName("secp256k1").toASN1Primitive()));
seq.addObject(new DERTaggedObject(1, new DERBitString(getPubKey())));
seq.close();
return baos.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e); // Cannot happen, writing to memory stream.
}
/** A constructor variant with BigInteger pubkey. See {@link ECKey#ECKey(BigInteger, byte[])}. */
public ECKey(BigInteger privKey, BigInteger pubKey) {
this(privKey, Utils.bigIntegerToBytes(pubKey, 65));
}
/**
* Creates an ECKey given only the private key bytes. This is the same as using the BigInteger constructor, but
* is more convenient if you are importing a key from elsewhere. The public key will be automatically derived
* from the private key.
*/
public ECKey(byte[] privKeyBytes, byte[] pubKey) {
this(privKeyBytes == null ? null : new BigInteger(1, privKeyBytes), pubKey);
}
/**
* Create a new ECKey with an encrypted private key, a public key and a KeyCrypter.
*
* @param encryptedPrivateKey The private key, encrypted,
* @param pubKey The keys public key
* @param keyCrypter The KeyCrypter that will be used, with an AES key, to encrypt and decrypt the private key
*/
public ECKey(EncryptedPrivateKey encryptedPrivateKey, byte[] pubKey, KeyCrypter keyCrypter) {
this((byte[])null, pubKey);
this.keyCrypter = Preconditions.checkNotNull(keyCrypter);
this.encryptedPrivateKey = encryptedPrivateKey;
}
/**
@ -174,24 +200,31 @@ public class ECKey implements Serializable {
private ECKey(BigInteger privKey, byte[] pubKey) {
this(privKey, pubKey, false);
}
/** Creates an ECKey given the private key only. The public key is calculated from it (this is slow) */
public ECKey(BigInteger privKey) {
this(privKey, (byte[])null);
}
/** A constructor variant with BigInteger pubkey. See {@link ECKey#ECKey(BigInteger, byte[])}. */
public ECKey(BigInteger privKey, BigInteger pubKey) {
this(privKey, Utils.bigIntegerToBytes(pubKey, 65));
}
/**
* Creates an ECKey given only the private key bytes. This is the same as using the BigInteger constructor, but
* is more convenient if you are importing a key from elsewhere. If not provided the public key will be
* automatically derived from the private key.
* Output this ECKey as an ASN.1 encoded private key, as understood by OpenSSL or used by the BitCoin reference
* implementation in its wallet storage format.
*/
public ECKey(byte[] privKeyBytes, byte[] pubKey) {
this(privKeyBytes == null ? null : new BigInteger(1, privKeyBytes), pubKey);
public byte[] toASN1() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream(400);
// ASN1_SEQUENCE(EC_PRIVATEKEY) = {
// ASN1_SIMPLE(EC_PRIVATEKEY, version, LONG),
// ASN1_SIMPLE(EC_PRIVATEKEY, privateKey, ASN1_OCTET_STRING),
// ASN1_EXP_OPT(EC_PRIVATEKEY, parameters, ECPKPARAMETERS, 0),
// ASN1_EXP_OPT(EC_PRIVATEKEY, publicKey, ASN1_BIT_STRING, 1)
// } ASN1_SEQUENCE_END(EC_PRIVATEKEY)
DERSequenceGenerator seq = new DERSequenceGenerator(baos);
seq.addObject(new ASN1Integer(1)); // version
seq.addObject(new DEROctetString(priv.toByteArray()));
seq.addObject(new DERTaggedObject(0, SECNamedCurves.getByName("secp256k1").toASN1Primitive()));
seq.addObject(new DERTaggedObject(1, new DERBitString(getPubKey())));
seq.close();
return baos.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e); // Cannot happen, writing to memory stream.
}
}
/**
@ -236,6 +269,10 @@ public class ECKey implements Serializable {
return b.toString();
}
/**
* Produce a string rendering of the ECKey INCLUDING the private key.
* Unless you absolutely need the private key it is better for security reasons to just use toString().
*/
public String toStringWithPrivate() {
StringBuilder b = new StringBuilder();
b.append(toString());
@ -254,6 +291,19 @@ public class ECKey implements Serializable {
return new Address(params, hash160);
}
/**
* Clears all the ECKey private key contents from memory.
* WARNING - this method irreversibly deletes the private key information.
* It turns the ECKEy into a watch only key.
*/
public void clearPrivateKey() {
priv = BigInteger.ZERO;
if (encryptedPrivateKey != null) {
encryptedPrivateKey.clear();
}
}
/**
* Groups the two components that make up a signature, and provides a way to encode to DER form, which is
* how ECDSA signatures are represented when embedded in other data structures in the Bitcoin protocol. The raw
@ -291,13 +341,45 @@ public class ECKey implements Serializable {
* 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#signToDER(Sha256Hash)} instead. However sometimes
* the independent components can be useful, for instance, if you're doing to do further EC maths on them.
* @throws IllegalStateException if this ECKey doesn't have a private part.
* @throws KeyCrypterException if this ECKey doesn't have a private part.
*/
public ECDSASignature sign(Sha256Hash input) {
if (priv == null)
throw new IllegalStateException("This ECKey does not have the private key necessary for signing.");
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#signToDER(Sha256Hash)} 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 this ECKey doesn't have a private part.
*/
public ECDSASignature sign(Sha256Hash input, KeyParameter aesKey) throws KeyCrypterException {
// The private key bytes to use for signing.
BigInteger privateKeyForSigning;
if (isEncrypted()) {
// The private key needs decrypting before use.
if (aesKey == null) {
throw new KeyCrypterException("This ECKey is encrypted but no decryption key has been supplied.");
}
if (keyCrypter == null) {
throw new KeyCrypterException("There is no KeyCrypter to decrypt the private key for signing.");
}
privateKeyForSigning = new BigInteger(1, keyCrypter.decrypt(encryptedPrivateKey, aesKey));
} else {
// No decryption of private key required.
if (priv == null) {
throw new KeyCrypterException("This ECKey does not have the private key necessary for signing.");
} else {
privateKeyForSigning = priv;
}
}
ECDSASigner signer = new ECDSASigner();
ECPrivateKeyParameters privKey = new ECPrivateKeyParameters(priv, ecParams);
ECPrivateKeyParameters privKey = new ECPrivateKeyParameters(privateKeyForSigning, ecParams);
signer.init(true, privKey);
BigInteger[] sigs = signer.generateSignature(input.getBytes());
return new ECDSASignature(sigs[0], sigs[1]);
@ -328,7 +410,7 @@ public class ECKey implements Serializable {
} catch (NullPointerException e) {
// Bouncy Castle contains a bug that can cause NPEs given specially crafted signatures. Those signatures
// are inherently invalid/attack sigs so we just fail them here rather than crash the thread.
System.err.println("Caught NPE inside bouncy castle: " + e);
log.error("Caught NPE inside bouncy castle");
e.printStackTrace();
return false;
}
@ -347,7 +429,6 @@ public class ECKey implements Serializable {
return ECKey.verify(data, signature, pub);
}
private static BigInteger extractPrivateKeyFromASN1(byte[] asn1privkey) {
// To understand this code, see the definition of the ASN.1 format for EC private keys in the OpenSSL source
// code in ec_asn1.c:
@ -379,13 +460,25 @@ public class ECKey implements Serializable {
* encoded string.
*
* @throws IllegalStateException if this ECKey does not have the private part.
* @throws KeyCryptException if this ECKey is encrypted and no AESKey is provided or it does not decrypt the ECKey.
*/
public String signMessage(String message) {
public String signMessage(String message) throws KeyCrypterException {
return signMessage(message, null);
}
/**
* Signs a text message using the standard Bitcoin messaging signing format and returns the signature as a base64
* encoded string.
*
* @throws IllegalStateException if this ECKey does not have the private part.
* @throws KeyCryptException if this ECKey is encrypted and no AESKey is provided or it does not decrypt the ECKey.
*/
public String signMessage(String message, KeyParameter aesKey) throws KeyCrypterException {
if (priv == null)
throw new IllegalStateException("This ECKey does not have the private key necessary for signing.");
byte[] data = Utils.formatMessageForSigning(message);
Sha256Hash hash = Sha256Hash.createDouble(data);
ECDSASignature sig = sign(hash);
ECDSASignature sig = sign(hash, aesKey);
// Now we have to work backwards to figure out the recId needed to recover the signature.
int recId = -1;
for (int i = 0; i < 4; i++) {
@ -608,4 +701,113 @@ public class ECKey implements Serializable {
// avoid picking up the type code (compressed vs uncompressed) which is tacked on the end.
return (pub[0] & 0xFF) | ((pub[1] & 0xFF) << 8) | ((pub[2] & 0xFF) << 16) | ((pub[3] & 0xFF) << 24);
}
/**
* 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.
* To be secure you need to clear the original, unencrypted private key bytes.
*
* @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 {
Preconditions.checkNotNull(keyCrypter);
EncryptedPrivateKey encryptedPrivateKey = keyCrypter.encrypt(getPrivKeyBytes(), aesKey);
return new ECKey(encryptedPrivateKey, getPubKey(), keyCrypter);
}
/**
* Create a decrypted private key with the keyCrypter and AES key supplied.
*
* @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).
* @return unencryptedKey
*/
public ECKey decrypt(KeyCrypter keyCrypter, KeyParameter aesKey) throws KeyCrypterException {
Preconditions.checkNotNull(keyCrypter);
// 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");
}
// Decrypt the private key.
byte[] unencryptedPrivateKey = keyCrypter.decrypt(encryptedPrivateKey, aesKey);
return new ECKey(unencryptedPrivateKey, getPubKey());
}
/**
* Check that it is possible to decrypt the key with the keyCrypter and that the original key is returned.
*
* 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.
* See {@link Wallet#encrypt(KeyCrypter keyCrypter, KeyParameter aesKey)} for example usage.
*
* @returns 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) {
String genericErrorText = "The check that encryption could be reversed failed for key " + originalKey.toString() + ". ";
try {
ECKey rebornUnencryptedKey = encryptedKey.decrypt(keyCrypter, aesKey);
if (rebornUnencryptedKey == null) {
log.error(genericErrorText + "The test decrypted key was missing.");
return false;
}
byte[] originalPrivateKeyBytes = originalKey.getPrivKeyBytes();
if (originalPrivateKeyBytes != null) {
if (rebornUnencryptedKey.getPrivKeyBytes() == null) {
log.error(genericErrorText + "The test decrypted key was missing.");
return false;
} else {
if (originalPrivateKeyBytes.length != rebornUnencryptedKey.getPrivKeyBytes().length) {
log.error(genericErrorText + "The test decrypted private key was a different length to the original.");
return false;
} else {
for (int i = 0; i < originalPrivateKeyBytes.length; i++) {
if (originalPrivateKeyBytes[i] != rebornUnencryptedKey.getPrivKeyBytes()[i]) {
log.error(genericErrorText + "Byte " + i + " of the private key did not match the original.");
return false;
}
}
}
}
}
} catch (KeyCrypterException kce) {
log.error(kce.getMessage());
return false;
}
// Key can successfully be decrypted.
return true;
}
/**
* 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.
*/
public boolean isEncrypted() {
return keyCrypter != null && encryptedPrivateKey != null && encryptedPrivateKey.getEncryptedBytes() != null && encryptedPrivateKey.getEncryptedBytes().length > 0;
}
/**
* @return The encryptedPrivateKey (containing the encrypted private key bytes and initialisation vector) for this ECKey,
* or null if the ECKey is not encrypted.
*/
public EncryptedPrivateKey getEncryptedPrivateKey() {
if (encryptedPrivateKey == null) {
return null;
} else {
return encryptedPrivateKey.clone();
}
}
/**
* @return The KeyCrypter that was used to encrypt to encrypt this ECKey. You need this to decrypt the ECKey.
*/
public KeyCrypter getKeyCrypter() {
return keyCrypter;
}
}

View File

@ -20,6 +20,7 @@ import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
import com.google.common.base.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import java.io.*;
import java.math.BigInteger;
@ -697,6 +698,21 @@ public class Transaction extends ChildMessage implements Serializable {
* @param wallet A wallet is required to fetch the keys needed for signing.
*/
public synchronized void signInputs(SigHash hashType, Wallet wallet) throws ScriptException {
signInputs(hashType, wallet, null);
}
/**
* Once a transaction has some inputs and outputs added, the signatures in the inputs can be calculated. The
* signature is over the transaction itself, to prove the redeemer actually created that transaction,
* so we have to do this step last.<p>
* <p/>
* This method is similar to SignatureHash in script.cpp
*
* @param hashType This should always be set to SigHash.ALL currently. Other types are unused.
* @param wallet A wallet is required to fetch the keys needed for signing.
* @param aesKey The AES key to use to decrypt the key before signing. Null if no decryption is required.
*/
public synchronized void signInputs(SigHash hashType, Wallet wallet, KeyParameter aesKey) throws ScriptException {
Preconditions.checkState(inputs.size() > 0);
Preconditions.checkState(outputs.size() > 0);
@ -733,7 +749,7 @@ public class Transaction extends ChildMessage implements Serializable {
try {
// Usually 71-73 bytes.
ByteArrayOutputStream bos = new UnsafeByteArrayOutputStream(73);
bos.write(key.sign(hash).encodeToDER());
bos.write(key.sign(hash, aesKey).encodeToDER());
bos.write((hashType.ordinal() + 1) | (anyoneCanPay ? 0x80 : 0));
signatures[i] = bos.toByteArray();
bos.close();
@ -992,7 +1008,7 @@ public class Transaction extends ChildMessage implements Serializable {
maybeParse();
out.defaultWriteObject();
}
/**
* Gets the count of regular SigOps in this transactions
*/

View File

@ -16,8 +16,14 @@
package com.google.bitcoin.core;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.spongycastle.crypto.params.KeyParameter;
import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
import com.google.bitcoin.core.WalletTransaction.Pool;
import com.google.bitcoin.crypto.EncryptedPrivateKey;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.store.WalletProtobufSerializer;
import com.google.bitcoin.utils.Locks;
import com.google.common.base.Objects;
@ -25,6 +31,7 @@ import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ListenableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -169,7 +176,7 @@ public class Wallet implements Serializable, BlockChainListener {
/**
* A list of public/private EC keys owned by this user. Access it using addKey[s], hasKey[s] and findPubKeyFromHash.
*/
public final ArrayList<ECKey> keychain;
public ArrayList<ECKey> keychain;
private final NetworkParameters params;
@ -191,6 +198,7 @@ public class Wallet implements Serializable, BlockChainListener {
// A listener that relays confidence changes from the transaction confidence object to the wallet event listener,
// as a convenience to API users so they don't have to register on every transaction themselves.
private transient TransactionConfidence.Listener txConfidenceListener;
// If a TX hash appears in this set then notifyNewBestBlock will ignore it, as its confidence was already set up
// in receive() via Transaction.setBlockAppearance(). As the BlockChain always calls notifyNewBestBlock even if
// it sent transactions to the wallet, without this we'd double count.
@ -280,11 +288,36 @@ public class Wallet implements Serializable, BlockChainListener {
private transient CoinSelector coinSelector = new DefaultCoinSelector();
/**
* The keyCrypter for the wallet. This specifies the algorithm used for encrypting and decrypting the private keys.
*/
private KeyCrypter keyCrypter;
/**
* The wallet version. This is an int that can be used to track breaking changes in the wallet format.
* You can also use it to detect wallets that come from the future (ie they contain features you
* do not know how to deal with).
*/
private int version;
/**
* A description for the wallet.
*/
String description;
/**
* Creates a new, empty wallet with no keys and no transactions. If you want to restore a wallet from disk instead,
* see loadFromFile.
*/
public Wallet(NetworkParameters params) {
this(params, null);
}
/**
* Create a wallet with a keyCrypter to use in encrypting and decrypting keys.
*/
public Wallet(NetworkParameters params, KeyCrypter keyCrypter) {
this.keyCrypter = keyCrypter;
this.params = checkNotNull(params);
keychain = new ArrayList<ECKey>();
unspent = new HashMap<Sha256Hash, Transaction>();
@ -299,6 +332,7 @@ public class Wallet implements Serializable, BlockChainListener {
private void createTransientState() {
ignoreNextNewBlock = new HashSet<Sha256Hash>();
txConfidenceListener = new TransactionConfidence.Listener() {
@Override
public void onConfidenceChanged(Transaction tx) {
lock.lock();
// The invokers unlock us immediately so if an exception is thrown, the lock will be already open.
@ -1206,7 +1240,7 @@ public class Wallet implements Serializable, BlockChainListener {
addWalletTransaction(Pool.DEAD, doubleSpend);
// Inform the event listeners of the newly dead tx.
doubleSpend.getConfidence().setOverridingTransaction(tx);
}
}
}
/**
@ -1657,6 +1691,12 @@ public class Wallet implements Serializable, BlockChainListener {
*/
public BigInteger fee = BigInteger.ZERO;
/**
* The AES key to use to decrypt the private keys before signing.
* If null then no decryption will be performed and if decryption is required an exception will be thrown.
*/
public KeyParameter aesKey = null;
// Tracks if this has been passed to wallet.completeTx already: just a safety check.
private boolean completed;
@ -1867,7 +1907,7 @@ public class Wallet implements Serializable, BlockChainListener {
// Now sign the inputs, thus proving that we are entitled to redeem the connected outputs.
try {
req.tx.signInputs(Transaction.SigHash.ALL, this);
req.tx.signInputs(Transaction.SigHash.ALL, this, req.aesKey);
} catch (ScriptException e) {
// If this happens it means an output script in a wallet tx could not be understood. That should never
// happen, if it does it means the wallet has got into an inconsistent state.
@ -1947,6 +1987,14 @@ public class Wallet implements Serializable, BlockChainListener {
// TODO: Consider making keys a sorted list or hashset so membership testing is faster.
for (final ECKey key : keys) {
if (keychain.contains(key)) continue;
// If the key has a keyCrypter that does not match the Wallet's then a KeyCrypterException is thrown.
// This is done because only one keyCrypter is persisted per Wallet and hence all the keys must be homogenous.
if (keyCrypter != null && keyCrypter.getUnderstoodEncryptionType() != EncryptionType.UNENCRYPTED) {
if ( key.isEncrypted() && !keyCrypter.equals(key.getKeyCrypter())) {
throw new KeyCrypterException("Cannot add key " + key.toString() + " because the keyCrypter does not match the wallets. Keys must be homogenous.");
}
}
keychain.add(key);
added++;
}
@ -1956,6 +2004,7 @@ public class Wallet implements Serializable, BlockChainListener {
} finally {
lock.unlock();
}
for (ECKey key : keys) {
// TODO: Change this interface to be batch-oriented.
for (WalletEventListener listener : eventListeners) {
@ -2146,6 +2195,10 @@ public class Wallet implements Serializable, BlockChainListener {
builder.append("\nDEAD:\n");
toStringHelper(builder, dead, chain);
}
// Add the keyCrypter so that any setup parameters are in the wallet toString.
if (this.keyCrypter != null) {
builder.append("\n keyCrypter: " + keyCrypter.toString());
}
return builder.toString();
} finally {
lock.unlock();
@ -2574,7 +2627,265 @@ public class Wallet implements Serializable, BlockChainListener {
lock.unlock();
}
}
/**
* Encrypt the wallet using the KeyCrypter and the AES key.
*
* @param keyCrypter The KeyCrypter that specifies how to encrypt/ decrypt a key
* @param aesKey AES key to use (normally created using KeyCrypter#deriveKey and cached as it is time consuming to create from a password)
* @throws KeyCrypterException Thrown if the wallet encryption fails. If so, the wallet state is unchanged.
*/
public void encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) {
lock.lock();
try {
Preconditions.checkNotNull(keyCrypter);
// If the wallet is already encrypted then you cannot encrypt it again.
if (getEncryptionType() != EncryptionType.UNENCRYPTED) {
throw new IllegalStateException("Wallet is already encrypted");
}
// Create a new arraylist that will contain the encrypted keys
ArrayList<ECKey> encryptedKeyChain = new ArrayList<ECKey>();
for (ECKey key : keychain) {
if (key.isEncrypted()) {
// Key is already encrypted - add as is.
encryptedKeyChain.add(key);
} else {
// Encrypt the key.
ECKey encryptedKey = key.encrypt(keyCrypter, aesKey);
// Check that the encrypted key can be successfully decrypted.
// This is done as it is a critical failure if the private key cannot be decrypted successfully
// (all bitcoin controlled by that private key is lost forever).
// For a correctly constructed keyCrypter the encryption should always be reversible so it is just being as cautious as possible.
if (!ECKey.encryptionIsReversible(key, encryptedKey, keyCrypter, aesKey)) {
// Abort encryption
throw new KeyCrypterException("The key " + key.toString() + " cannot be successfully decrypted after encryption so aborting wallet encryption.");
}
encryptedKeyChain.add(encryptedKey);
}
}
// Now ready to use the encrypted keychain so go through the old keychain clearing all the unencrypted private keys.
// (This is to avoid the possibility of key recovery from memory).
for (ECKey key : keychain) {
if (!key.isEncrypted()) {
key.clearPrivateKey();
}
}
// Replace the old keychain with the encrypted one.
keychain = encryptedKeyChain;
// The wallet is now encrypted.
this.keyCrypter = keyCrypter;
if (autosaveToFile != null) {
autoSave();
}
} finally {
lock.unlock();
}
}
/**
* Decrypt the wallet with the wallets keyCrypter and AES key.
*
* @param aesKey AES key to use (normally created using KeyCrypter#deriveKey and cached as it is time consuming to create from a password)
* @throws KeyCrypterException Thrown if the wallet decryption fails. If so, the wallet state is unchanged.
*/
public void decrypt(KeyParameter aesKey) {
lock.lock();
try {
// Check the wallet is already encrypted - you cannot decrypt an unencrypted wallet.
if (getEncryptionType() == EncryptionType.UNENCRYPTED) {
throw new IllegalStateException("Wallet is already decrypted");
}
// Check that the wallet keyCrypter is non-null.
// This is set either at construction (if an encrypted wallet is created) or by wallet encryption.
Preconditions.checkNotNull(keyCrypter);
// Create a new arraylist that will contain the decrypted keys
ArrayList<ECKey> decryptedKeyChain = new ArrayList<ECKey>();
for (ECKey key : keychain) {
// Decrypt the key.
if (!key.isEncrypted()) {
// Not encrypted - add to chain as is.
decryptedKeyChain.add(key);
} else {
ECKey decryptedECKey = key.decrypt(keyCrypter, aesKey);
decryptedKeyChain.add(decryptedECKey);
}
}
// Replace the old keychain with the unencrypted one.
keychain = decryptedKeyChain;
// The wallet is now unencrypted.
keyCrypter = null;
if (autosaveToFile != null) {
autoSave();
}
} finally {
lock.unlock();
}
}
/**
* Create a new encrypted ECKey and add it to the wallet.
*
* @param keyCrypter The keyCrypter to use in encrypting the new key
* @param aesKey The AES key to use to encrypt the new key
* @return ECKey the new, encrypted ECKey
*/
public ECKey addNewEncryptedKey(KeyCrypter keyCrypter, KeyParameter aesKey) {
lock.lock();
try {
ECKey newKey = (new ECKey()).encrypt(keyCrypter, aesKey);
addKey(newKey);
return newKey;
} finally {
lock.unlock();
}
}
/**
* Check whether the password can decrypt the first key in the wallet.
* This can be used to check the validity of an entered password.
*
* @returns boolean true if password supplied can decrypt the first private key in the wallet, false otherwise.
*/
public boolean checkPassword(CharSequence password) {
lock.lock();
try {
if (keyCrypter == null) {
// The password cannot decrypt anything as the keyCrypter is null.
return false;
}
return checkAESKey(keyCrypter.deriveKey(password));
} finally {
lock.unlock();
}
}
/**
* Check whether the AES key can decrypt the first encrypted key in the wallet.
*
* @returns boolean true if AES key supplied can decrypt the first encrypted private key in the wallet, false otherwise.
*/
public boolean checkAESKey(KeyParameter aesKey) {
lock.lock();
try {
// If no keys then cannot decrypt.
if (!getKeys().iterator().hasNext()) {
return false;
}
// Find the first encrypted key in the wallet.
ECKey firstEncryptedECKey = null;
Iterator<ECKey> iterator = getKeys().iterator();
while (iterator.hasNext() && firstEncryptedECKey == null) {
ECKey loopECKey = iterator.next();
if (loopECKey.isEncrypted()) {
firstEncryptedECKey = loopECKey;
}
}
// There are no encrypted keys in the wallet.
if (firstEncryptedECKey == null) {
return false;
}
String originalAddress = firstEncryptedECKey.toAddress(getNetworkParameters()).toString();
if (firstEncryptedECKey != null && firstEncryptedECKey.isEncrypted() && firstEncryptedECKey.getEncryptedPrivateKey() != null) {
try {
ECKey rebornKey = firstEncryptedECKey.decrypt(keyCrypter, aesKey);
// Check that the decrypted private key's address is correct ie it decrypted accurately.
String rebornAddress = rebornKey.toAddress(getNetworkParameters()).toString();
return originalAddress.equals(rebornAddress);
} catch (KeyCrypterException ede) {
// The AES key supplied is incorrect.
return false;
}
}
return false;
} finally {
lock.unlock();
}
}
/**
* Get the wallet's KeyCrypter.
* (Used in encrypting/ decrypting an ECKey).
*/
public KeyCrypter getKeyCrypter() {
lock.lock();
try {
return keyCrypter;
} finally {
lock.unlock();
}
}
/**
* Get the type of encryption used for this wallet.
*
* (This is a convenience method - the encryption type is actually stored in the keyCrypter).
*/
public EncryptionType getEncryptionType() {
lock.lock();
try {
if (keyCrypter == null) {
// Unencrypted wallet.
return EncryptionType.UNENCRYPTED;
} else {
return keyCrypter.getUnderstoodEncryptionType();
}
} finally {
lock.unlock();
}
}
/**
* Get the version of the Wallet.
* This is an int you can use to indicate which versions of wallets your code understands,
* and which come from the future (and hence cannot be safely loaded).
*/
public int getVersion() {
return version;
}
/**
* Set the version number of the wallet. See {@link Wallet#getVersion()}.
*/
public void setVersion(int version) {
this.version = version;
}
/**
* Set the description of the wallet.
* This is a Unicode encoding string typically entered by the user as descriptive text for the wallet.
*/
public void setDescription(String description) {
this.description = description;
}
/**
* Get the description of the wallet. See {@link Wallet#setDescription(String))}
* @return
*/
public String getDescription() {
return description;
}
/**
* Gets the number of elements that will be added to a bloom filter returned by getBloomFilter
*/

View File

@ -0,0 +1,137 @@
/**
* Copyright 2013 Jim Burton.
*
* Licensed under the MIT license (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://opensource.org/licenses/mit-license.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.crypto;
import java.util.Arrays;
import com.google.bitcoin.core.BlockChain;
import com.google.bitcoin.core.Wallet;
import com.google.common.base.Preconditions;
/**
* <p>An EncryptedPrivateKey contains the information produced after encrypting the private key bytes of an ECKey.</p>
*
* <p>It contains two member variables - initialisationVector and encryptedPrivateBytes. The initialisationVector is
* a randomly chosen list of bytes that were used to initialise the AES block cipher when the private key bytes were encrypted.
* You need these for decryption. The encryptedPrivateBytes are the result of AES encrypting the private keys using
* an AES key that is drrived from a user entered password. You need the password to recreate the AES key in order
* to decrypt these bytes.</p>
*/
public class EncryptedPrivateKey {
private byte[] initialisationVector = null;
private byte[] encryptedPrivateBytes = null;
/**
* Cloning constructor.
* @param encryptedPrivateKey EncryptedPrivateKey to clone.
*/
public EncryptedPrivateKey(EncryptedPrivateKey encryptedPrivateKey) {
Preconditions.checkNotNull(encryptedPrivateKey);
setInitialisationVector(encryptedPrivateKey.getInitialisationVector());
setEncryptedPrivateBytes(encryptedPrivateKey.getEncryptedBytes());
}
/**
* @param iv
* @param encryptedPrivateKeys
*/
public EncryptedPrivateKey(byte[] initialisationVector, byte[] encryptedPrivateKeys) {
setInitialisationVector(initialisationVector);
setEncryptedPrivateBytes(encryptedPrivateKeys);
}
public byte[] getInitialisationVector() {
return initialisationVector;
}
/**
* Set the initialisationVector, cloning the bytes.
*
* @param initialisationVector
*/
public void setInitialisationVector(byte[] initialisationVector) {
if (initialisationVector == null) {
this.initialisationVector = null;
return;
}
byte[] cloneIV = new byte[initialisationVector.length];
System.arraycopy(initialisationVector, 0, cloneIV, 0, initialisationVector.length);
this.initialisationVector = cloneIV;
}
public byte[] getEncryptedBytes() {
return encryptedPrivateBytes;
}
/**
* Set the encrypted private key bytes, cloning them.
*
* @param encryptedPrivateBytes
*/
public void setEncryptedPrivateBytes(byte[] encryptedPrivateBytes) {
if (encryptedPrivateBytes == null) {
this.encryptedPrivateBytes = null;
return;
}
this.encryptedPrivateBytes = Arrays.copyOf(encryptedPrivateBytes, encryptedPrivateBytes.length);
}
@Override
public EncryptedPrivateKey clone() {
return new EncryptedPrivateKey(getInitialisationVector(), getEncryptedBytes());
}
@Override
public int hashCode() {
return com.google.common.base.Objects.hashCode(encryptedPrivateBytes, initialisationVector);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final EncryptedPrivateKey other = (EncryptedPrivateKey) obj;
return com.google.common.base.Objects.equal(this.initialisationVector, other.initialisationVector)
&& com.google.common.base.Objects.equal(this.encryptedPrivateBytes, other.encryptedPrivateBytes);
}
@Override
public String toString() {
return "EncryptedPrivateKey [initialisationVector=" + Arrays.toString(initialisationVector) + ", encryptedPrivateKey=" + Arrays.toString(encryptedPrivateBytes) + "]";
}
/**
* Clears all the EncryptedPrivateKey contents from memory (overwriting all data including PRIVATE KEYS).
* WARNING - this method irreversibly deletes the private key information.
*/
public void clear() {
if (encryptedPrivateBytes != null) {
Arrays.fill(encryptedPrivateBytes, (byte)0);
}
if (initialisationVector != null) {
Arrays.fill(initialisationVector, (byte)0);
}
}
}

View File

@ -0,0 +1,68 @@
/**
* Copyright 2013 Jim Burton.
*
* Licensed under the MIT license (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://opensource.org/licenses/mit-license.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.crypto;
import java.io.Serializable;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.spongycastle.crypto.params.KeyParameter;
/**
* <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 EncryptedPrivateKey which contains the encryptedPrivateKey bytes and an initialisation vector.</p>
*<p>(3) To decrypt an EncryptedPrivateKey, 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 extends Serializable {
/**
* Return the EncryptionType enum value which denotes the type of encryption/ decryption that this KeyCrypter
* can understand.
*/
public 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
*/
public KeyParameter deriveKey(CharSequence password) throws KeyCrypterException;
/**
* Decrypt the provided encrypted bytes, converting them into unencrypted bytes.
*
* @throws KeyCrypterException if decryption was unsuccessful.
*/
public byte[] decrypt(EncryptedPrivateKey encryptedBytesToDecode, KeyParameter aesKey) throws KeyCrypterException;
/**
* Encrypt the supplied bytes, converting them into ciphertext.
*
* @param plainBytes
* @param aesKey
* @return encryptedPrivateKey An encryptedPrivateKey containing the encrypted bytes and an initialisation vector.
* @throws keyCrypterException if encryption was unsuccessful
*/
public EncryptedPrivateKey encrypt(byte[] plainBytes, KeyParameter aesKey) throws KeyCrypterException;
}

View File

@ -0,0 +1,22 @@
package com.google.bitcoin.crypto;
/**
* <p>Exception to provide the following to {@link EncrypterDecrypterOpenSSL}:</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 {
private static final long serialVersionUID = -4441989608332681377L;
public KeyCrypterException(String s) {
super(s);
}
public KeyCrypterException(String s, Throwable throwable) {
super(s, throwable);
}
}

View File

@ -0,0 +1,262 @@
/**
* Copyright 2013 Jim Burton.
*
* Licensed under the MIT license (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://opensource.org/licenses/mit-license.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.crypto;
import java.io.Serializable;
import java.security.SecureRandom;
import org.bitcoinj.wallet.Protos;
import org.bitcoinj.wallet.Protos.ScryptParameters;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.BufferedBlockCipher;
import org.spongycastle.crypto.engines.AESFastEngine;
import org.spongycastle.crypto.modes.CBCBlockCipher;
import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.crypto.params.ParametersWithIV;
import com.google.common.base.Preconditions;
import com.google.protobuf.ByteString;
import com.lambdaworks.crypto.SCrypt;
/**
* <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 KeyCrypterScrypt implements KeyCrypter, Serializable {
public Logger log = LoggerFactory.getLogger(KeyCrypterScrypt.class.getName());
private static final long serialVersionUID = 949662512049152670L;
/**
* 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;
transient private static SecureRandom secureRandom = new SecureRandom();
// Scrypt parameters.
transient private ScryptParameters scryptParameters;
/**
* Encryption/ Decryption using default parameters and a random salt
*/
public KeyCrypterScrypt() {
byte[] salt = new byte[SALT_LENGTH];
secureRandom.nextBytes(salt);
Protos.ScryptParameters.Builder scryptParametersBuilder = Protos.ScryptParameters.newBuilder().setSalt(ByteString.copyFrom(salt));
this.scryptParameters = scryptParametersBuilder.build();
}
/**
* 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 KeyCrypterScrypt(ScryptParameters scryptParameters) {
this.scryptParameters = Preconditions.checkNotNull(scryptParameters);
// Check there is a non-empty salt.
// (Some early MultiBit wallets has a missing salt so it is not a hard fail).
if (scryptParameters.getSalt() == null
|| scryptParameters.getSalt().toByteArray() == null
|| scryptParameters.getSalt().toByteArray().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().toByteArray();
} else {
// Warn the user that they are not using a salt.
// (Some early MultiBit wallets had a blank salt).
log.warn("You are using a ScryptParameters with no salt. Your encryption may be vulnerable to a dictionary attack.");
}
byte[] keyBytes = SCrypt.scrypt(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.
*
* @param plain The bytes to encrypt
* @param aesKey The AES key to use for encryption
* @return EncryptedPrivateKey containing IV and the encrypted private key
* @throws KeyCrypterException
*/
@Override
public EncryptedPrivateKey encrypt(byte[] plainBytes, KeyParameter aesKey) throws KeyCrypterException {
Preconditions.checkNotNull(plainBytes);
Preconditions.checkNotNull(aesKey);
try {
// Generate iv - each encryption call has a different iv.
byte[] iv = new byte[BLOCK_LENGTH];
secureRandom.nextBytes(iv);
ParametersWithIV keyWithIv = new ParametersWithIV(aesKey, iv);
// Encrypt using AES.
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine()));
cipher.init(true, keyWithIv);
byte[] encryptedBytes = new byte[cipher.getOutputSize(plainBytes.length)];
int length = cipher.processBytes(plainBytes, 0, plainBytes.length, encryptedBytes, 0);
cipher.doFinal(encryptedBytes, length);
return new EncryptedPrivateKey(iv, encryptedBytes);
} catch (Exception e) {
throw new KeyCrypterException("Could not encrypt bytes.", e);
}
}
/**
* Decrypt bytes previously encrypted with this class.
*
* @param privateKeyToDecode The private key to decrypt
* @param aesKey The AES key to use for decryption
* @return The decrypted bytes
* @throws KeyCrypterException if bytes could not be decoded to a valid key
*/
@Override
public byte[] decrypt(EncryptedPrivateKey privateKeyToDecode, KeyParameter aesKey) throws KeyCrypterException {
Preconditions.checkNotNull(privateKeyToDecode);
Preconditions.checkNotNull(aesKey);
try {
ParametersWithIV keyWithIv = new ParametersWithIV(new KeyParameter(aesKey.getKey()), privateKeyToDecode.getInitialisationVector());
// Decrypt the message.
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine()));
cipher.init(false, keyWithIv);
byte[] cipherBytes = privateKeyToDecode.getEncryptedBytes();
int minimumSize = cipher.getOutputSize(cipherBytes.length);
byte[] outputBuffer = new byte[minimumSize];
int length1 = cipher.processBytes(cipherBytes, 0, cipherBytes.length, outputBuffer, 0);
int length2 = cipher.doFinal(outputBuffer, length1);
int actualLength = length1 + length2;
byte[] decryptedBytes = new byte[actualLength];
System.arraycopy(outputBuffer, 0, decryptedBytes, 0, actualLength);
return decryptedBytes;
} catch (Exception 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 byte[] convertToByteArray(CharSequence charSequence) {
Preconditions.checkNotNull(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 "EncrypterDecrypterScrypt [scryptParameters=" + scryptParameters.toString() + "]";
}
@Override
public int hashCode() {
return com.google.common.base.Objects.hashCode(scryptParameters);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final KeyCrypterScrypt other = (KeyCrypterScrypt) obj;
return com.google.common.base.Objects.equal(this.scryptParameters, other.scryptParameters);
}
}

View File

@ -16,13 +16,15 @@
package com.google.bitcoin.store;
import com.google.bitcoin.core.NetworkParameters;
import com.google.bitcoin.core.Wallet;
import org.bitcoinj.wallet.Protos;
import java.util.Collection;
import java.util.Collections;
import org.bitcoinj.wallet.Protos;
import com.google.bitcoin.core.NetworkParameters;
import com.google.bitcoin.core.Wallet;
import com.google.bitcoin.crypto.KeyCrypter;
/**
* Optional helper for WalletProtobufSerializer that allows for serialization and deserialization of Wallet objects
* with extensions and corresponding extended Wallet classes. If you want to store proprietary data into the wallet,
@ -33,6 +35,10 @@ public class WalletExtensionSerializer {
return new Wallet(params);
}
public Wallet newWallet(NetworkParameters params, KeyCrypter keyCrypter) {
return new Wallet(params, keyCrypter);
}
public void readExtension(Wallet wallet, Protos.Extension extProto) {
if (extProto.getMandatory()) {
throw new IllegalArgumentException("Unknown mandatory extension in the wallet: " + extProto.getId());

View File

@ -16,15 +16,6 @@
package com.google.bitcoin.store;
import com.google.bitcoin.core.*;
import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
import com.google.common.base.Preconditions;
import com.google.protobuf.ByteString;
import com.google.protobuf.TextFormat;
import org.bitcoinj.wallet.Protos;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -33,6 +24,33 @@ import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import org.bitcoinj.wallet.Protos;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.bitcoinj.wallet.Protos.Key.Type;
import com.google.bitcoin.crypto.EncryptedPrivateKey;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.NetworkParameters;
import com.google.bitcoin.core.PeerAddress;
import com.google.bitcoin.core.Sha256Hash;
import com.google.bitcoin.core.Transaction;
import com.google.bitcoin.core.TransactionConfidence;
import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
import com.google.bitcoin.core.TransactionInput;
import com.google.bitcoin.core.TransactionOutPoint;
import com.google.bitcoin.core.TransactionOutput;
import com.google.bitcoin.core.Wallet;
import com.google.bitcoin.core.WalletTransaction;
import com.google.common.base.Preconditions;
import com.google.protobuf.ByteString;
import com.google.protobuf.TextFormat;
/**
* Serialize and de-serialize a wallet to a byte stream containing a
* <a href="http://code.google.com/apis/protocolbuffers/docs/overview.html">protocol buffer</a>. Protocol buffers are
@ -56,8 +74,8 @@ public class WalletProtobufSerializer {
private static final Logger log = LoggerFactory.getLogger(WalletProtobufSerializer.class);
// Used for de-serialization
private Map<ByteString, Transaction> txMap;
private WalletExtensionSerializer helper;
protected Map<ByteString, Transaction> txMap;
protected WalletExtensionSerializer helper;
public WalletProtobufSerializer() {
txMap = new HashMap<ByteString, Transaction>();
@ -72,7 +90,6 @@ public class WalletProtobufSerializer {
this.helper = h;
}
/**
* Formats the given wallet (transactions and keys) to the given output stream in protocol buffer format.<p>
*
@ -102,39 +119,86 @@ public class WalletProtobufSerializer {
public Protos.Wallet walletToProto(Wallet wallet) {
Protos.Wallet.Builder walletBuilder = Protos.Wallet.newBuilder();
walletBuilder.setNetworkIdentifier(wallet.getNetworkParameters().getId());
if (wallet.getDescription() != null) {
walletBuilder.setDescription(wallet.getDescription());
}
for (WalletTransaction wtx : wallet.getWalletTransactions()) {
Protos.Transaction txProto = makeTxProto(wtx);
walletBuilder.addTransaction(txProto);
}
for (ECKey key : wallet.getKeys()) {
Protos.Key.Builder buf = Protos.Key.newBuilder().setCreationTimestamp(key.getCreationTimeSeconds() * 1000)
Protos.Key.Builder keyBuilder = Protos.Key.newBuilder().setCreationTimestamp(key.getCreationTimeSeconds() * 1000)
// .setLabel() TODO
.setType(Protos.Key.Type.ORIGINAL);
if (key.getPrivKeyBytes() != null)
buf.setPrivateKey(ByteString.copyFrom(key.getPrivKeyBytes()));
keyBuilder.setPrivateKey(ByteString.copyFrom(key.getPrivKeyBytes()));
EncryptedPrivateKey encryptedPrivateKey = key.getEncryptedPrivateKey();
if (encryptedPrivateKey != null) {
// Key is encrypted.
Protos.EncryptedPrivateKey.Builder encryptedKeyBuilder = Protos.EncryptedPrivateKey.newBuilder()
.setEncryptedPrivateKey(ByteString.copyFrom(encryptedPrivateKey.getEncryptedBytes()))
.setInitialisationVector(ByteString.copyFrom(encryptedPrivateKey.getInitialisationVector()));
if (key.getKeyCrypter() == null) {
throw new IllegalStateException("The encrypted key " + key.toString() + " has no KeyCrypter.");
} else {
// If it is a Scrypt + AES encrypted key, set the persisted key type.
if (key.getKeyCrypter().getUnderstoodEncryptionType() == Protos.Wallet.EncryptionType.ENCRYPTED_SCRYPT_AES) {
keyBuilder.setType(Protos.Key.Type.ENCRYPTED_SCRYPT_AES);
} else {
throw new IllegalArgumentException("The key " + key.toString() + " is encrypted with a KeyCrypter of type " + key.getKeyCrypter().getUnderstoodEncryptionType() +
". This WalletProtobufSerialiser does not understand that type of encryption.");
}
}
keyBuilder.setEncryptedPrivateKey(encryptedKeyBuilder);
}
// We serialize the public key even if the private key is present for speed reasons: we don't want to do
// lots of slow EC math to load the wallet, we prefer to store the redundant data instead. It matters more
// on mobile platforms.
buf.setPublicKey(ByteString.copyFrom(key.getPubKey()));
walletBuilder.addKey(buf);
keyBuilder.setPublicKey(ByteString.copyFrom(key.getPubKey()));
walletBuilder.addKey(keyBuilder);
}
// Populate the lastSeenBlockHash field.
Sha256Hash lastSeenBlockHash = wallet.getLastBlockSeenHash();
if (lastSeenBlockHash != null) {
walletBuilder.setLastSeenBlockHash(hashToByteString(lastSeenBlockHash));
walletBuilder.setLastSeenBlockHeight(wallet.getLastBlockSeenHeight());
}
// Populate the scrypt parameters.
KeyCrypter keyCrypter = wallet.getKeyCrypter();
if (keyCrypter == null) {
// The wallet is unencrypted.
walletBuilder.setEncryptionType(EncryptionType.UNENCRYPTED);
} else {
// The wallet is encrypted.
walletBuilder.setEncryptionType(keyCrypter.getUnderstoodEncryptionType());
if (keyCrypter instanceof KeyCrypterScrypt) {
KeyCrypterScrypt keyCrypterScrypt = (KeyCrypterScrypt) keyCrypter;
walletBuilder.setEncryptionParameters(keyCrypterScrypt.getScryptParameters());
} else {
// Some other form of encryption has been specified that we do not know how to persist.
throw new RuntimeException("The wallet has encryption of type '" + keyCrypter.getUnderstoodEncryptionType() + "' but this WalletProtobufSerializer does not know how to persist this.");
}
}
// Populate the wallet version.
walletBuilder.setVersion(wallet.getVersion());
Collection<Protos.Extension> extensions = helper.getExtensionsToWrite(wallet);
for(Protos.Extension ext : extensions) {
walletBuilder.addExtension(ext);
}
return walletBuilder.build();
}
private static Protos.Transaction makeTxProto(WalletTransaction wtx) {
protected static Protos.Transaction makeTxProto(WalletTransaction wtx) {
Transaction tx = wtx.getTransaction();
Protos.Transaction.Builder txBuilder = Protos.Transaction.newBuilder();
@ -193,7 +257,7 @@ public class WalletProtobufSerializer {
return txBuilder.build();
}
private static void writeConfidence(Protos.Transaction.Builder txBuilder,
protected static void writeConfidence(Protos.Transaction.Builder txBuilder,
TransactionConfidence confidence,
Protos.TransactionConfidence.Builder confidenceBuilder) {
synchronized (confidence) {
@ -206,8 +270,12 @@ public class WalletProtobufSerializer {
}
}
if (confidence.getConfidenceType() == ConfidenceType.DEAD) {
Sha256Hash overridingHash = confidence.getOverridingTransaction().getHash();
confidenceBuilder.setOverridingTransaction(hashToByteString(overridingHash));
// Copy in the overriding transaction, if available.
// (A dead coinbase transaction has no overriding transaction).
if (confidence.getOverridingTransaction() != null) {
Sha256Hash overridingHash = confidence.getOverridingTransaction().getHash();
confidenceBuilder.setOverridingTransaction(hashToByteString(overridingHash));
}
}
TransactionConfidence.Source source = confidence.getSource();
switch (source) {
@ -219,6 +287,7 @@ public class WalletProtobufSerializer {
confidenceBuilder.setSource(Protos.TransactionConfidence.Source.SOURCE_UNKNOWN); break;
}
}
for (ListIterator<PeerAddress> it = confidence.getBroadcastBy(); it.hasNext();) {
PeerAddress address = it.next();
Protos.PeerAddress proto = Protos.PeerAddress.newBuilder()
@ -231,11 +300,11 @@ public class WalletProtobufSerializer {
txBuilder.setConfidence(confidenceBuilder);
}
private static ByteString hashToByteString(Sha256Hash hash) {
public static ByteString hashToByteString(Sha256Hash hash) {
return ByteString.copyFrom(hash.getBytes());
}
private static Sha256Hash byteStringToHash(ByteString bs) {
public static Sha256Hash byteStringToHash(ByteString bs) {
return new Sha256Hash(bs.toByteArray());
}
@ -245,7 +314,6 @@ public class WalletProtobufSerializer {
*
* If the stream is invalid or the serialized wallet contains unsupported features,
* {@link IllegalArgumentException} is thrown.
*
*/
public Wallet readWallet(InputStream input) throws IOException {
// TODO: This method should throw more specific exception types than IllegalArgumentException.
@ -253,24 +321,48 @@ public class WalletProtobufSerializer {
// System.out.println(TextFormat.printToString(walletProto));
// Read the scrypt parameters that specify how encryption and decryption is performed.
KeyCrypter keyCrypter = null;
if (walletProto.hasEncryptionParameters()) {
Protos.ScryptParameters encryptionParameters = walletProto.getEncryptionParameters();
keyCrypter = new KeyCrypterScrypt(encryptionParameters);
}
NetworkParameters params = NetworkParameters.fromID(walletProto.getNetworkIdentifier());
Wallet wallet = helper.newWallet(params);
Wallet wallet = helper.newWallet(params, keyCrypter);
if (walletProto.hasDescription()) {
wallet.setDescription(walletProto.getDescription());
}
// Read all keys
for (Protos.Key keyProto : walletProto.getKeyList()) {
if (keyProto.getType() != Protos.Key.Type.ORIGINAL) {
throw new IllegalArgumentException("Unknown key type in wallet");
if (!(keyProto.getType() == Protos.Key.Type.ORIGINAL || keyProto.getType() == Protos.Key.Type.ENCRYPTED_SCRYPT_AES)) {
throw new IllegalArgumentException("Unknown key type in wallet, type = " + keyProto.getType());
}
byte[] privKey = null;
if (keyProto.hasPrivateKey()) {
privKey = keyProto.getPrivateKey().toByteArray();
byte[] privKey = keyProto.hasPrivateKey() ? keyProto.getPrivateKey().toByteArray() : null;
EncryptedPrivateKey encryptedPrivateKey = null;
if (keyProto.hasEncryptedPrivateKey()) {
Protos.EncryptedPrivateKey encryptedPrivateKeyProto = keyProto.getEncryptedPrivateKey();
encryptedPrivateKey = new EncryptedPrivateKey(encryptedPrivateKeyProto.getInitialisationVector().toByteArray(),
encryptedPrivateKeyProto.getEncryptedPrivateKey().toByteArray());
}
byte[] pubKey = keyProto.hasPublicKey() ? keyProto.getPublicKey().toByteArray() : null;
ECKey ecKey = new ECKey(privKey, pubKey);
ECKey ecKey = null;
if (keyCrypter != null && keyCrypter.getUnderstoodEncryptionType() != EncryptionType.UNENCRYPTED) {
// If the key is encrypted construct an ECKey using the encrypted private key bytes.
ecKey = new ECKey(encryptedPrivateKey, pubKey, keyCrypter);
} else {
// Construct an unencrypted private key.
ecKey = new ECKey(privKey, pubKey);
}
ecKey.setCreationTimeSeconds((keyProto.getCreationTimestamp() + 500) / 1000);
wallet.addKey(ecKey);
}
// Read all transactions and insert into the txMap.
for (Protos.Transaction txProto : walletProto.getTransactionList()) {
readTransaction(txProto, params);
@ -281,7 +373,7 @@ public class WalletProtobufSerializer {
WalletTransaction wtx = connectTransactionOutputs(txProto);
wallet.addWalletTransaction(wtx);
}
// Update the lastBlockSeenHash.
if (!walletProto.hasLastSeenBlockHash()) {
wallet.setLastBlockSeenHash(null);
@ -297,7 +389,11 @@ public class WalletProtobufSerializer {
for (Protos.Extension extProto : walletProto.getExtensionList()) {
helper.readExtension(wallet, extProto);
}
if (walletProto.hasVersion()) {
wallet.setVersion(walletProto.getVersion());
}
return wallet;
}
@ -310,7 +406,7 @@ public class WalletProtobufSerializer {
return Protos.Wallet.parseFrom(input);
}
private void readTransaction(Protos.Transaction txProto, NetworkParameters params) {
protected void readTransaction(Protos.Transaction txProto, NetworkParameters params) {
Transaction tx = new Transaction(params);
if (txProto.hasUpdatedAt()) {
tx.setUpdateTime(new Date(txProto.getUpdatedAt()));
@ -353,7 +449,7 @@ public class WalletProtobufSerializer {
txMap.put(txProto.getHash(), tx);
}
private WalletTransaction connectTransactionOutputs(org.bitcoinj.wallet.Protos.Transaction txProto) {
protected WalletTransaction connectTransactionOutputs(org.bitcoinj.wallet.Protos.Transaction txProto) {
Transaction tx = txMap.get(txProto.getHash());
WalletTransaction.Pool pool = WalletTransaction.Pool.valueOf(txProto.getPool().getNumber());
for (int i = 0 ; i < tx.getOutputs().size() ; i++) {
@ -376,7 +472,7 @@ public class WalletProtobufSerializer {
return new WalletTransaction(pool, tx);
}
private void readConfidence(Transaction tx, Protos.TransactionConfidence confidenceProto,
protected void readConfidence(Transaction tx, Protos.TransactionConfidence confidenceProto,
TransactionConfidence confidence) {
// We are lenient here because tx confidence is not an essential part of the wallet.
// If the tx has an unknown type of confidence, ignore.

File diff suppressed because it is too large Load Diff

View File

@ -16,19 +16,57 @@
package com.google.bitcoin.core;
import org.junit.Test;
import org.spongycastle.util.encoders.Hex;
import static com.google.bitcoin.core.Utils.reverseBytes;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.security.SignatureException;
import java.util.Arrays;
import static com.google.bitcoin.core.Utils.reverseBytes;
import static org.junit.Assert.*;
import org.bitcoinj.wallet.Protos;
import org.bitcoinj.wallet.Protos.ScryptParameters;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.util.encoders.Hex;
import com.google.bitcoin.crypto.EncryptedPrivateKey;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.bitcoin.utils.BriefLogFormatter;
import com.google.protobuf.ByteString;
public class ECKeyTest {
public Logger log = LoggerFactory.getLogger(ECKeyTest.class.getName());
private SecureRandom secureRandom;
private KeyCrypter keyCrypter;
private static CharSequence PASSWORD1 = "my hovercraft has eels";
private static CharSequence WRONG_PASSWORD = "it is a snowy day today";
@Before
public void setUp() throws Exception {
secureRandom = new SecureRandom();
byte[] salt = new byte[KeyCrypterScrypt.SALT_LENGTH];
secureRandom.nextBytes(salt);
Protos.ScryptParameters.Builder scryptParametersBuilder = Protos.ScryptParameters.newBuilder().setSalt(ByteString.copyFrom(salt));
ScryptParameters scryptParameters = scryptParametersBuilder.build();
keyCrypter = new KeyCrypterScrypt(scryptParameters);
BriefLogFormatter.init();
}
@Test
public void testSignatures() {
public void testSignatures() throws Exception {
// Test that we can construct an ECKey from a private key (deriving the public from the private), then signing
// a message with it.
BigInteger privkey = new BigInteger(1, Hex.decode("180cb41c7c600be951b5d3d0a7334acc7506173875834f7a6c4c786a28fcbb19"));
@ -43,7 +81,7 @@ public class ECKeyTest {
}
@Test
public void testASN1Roundtrip() {
public void testASN1Roundtrip() throws Exception {
byte[] privkeyASN1 = Hex.decode(
"3082011302010104205c0b98e524ad188ddef35dc6abba13c34a351a05409e5d285403718b93336a4aa081a53081a2020101302c06072a8648ce3d0101022100fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f300604010004010704410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8022100fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141020101a144034200042af7a2aafe8dafd7dc7f9cfb58ce09bda7dce28653ab229b98d1d3d759660c672dd0db18c8c2d76aa470448e876fc2089ab1354c01a6e72cefc50915f4a963ee");
ECKey decodedKey = ECKey.fromASN1(privkeyASN1);
@ -73,7 +111,7 @@ public class ECKeyTest {
}
@Test
public void testKeyPairRoundtrip() {
public void testKeyPairRoundtrip() throws Exception {
byte[] privkeyASN1 = Hex.decode(
"3082011302010104205c0b98e524ad188ddef35dc6abba13c34a351a05409e5d285403718b93336a4aa081a53081a2020101302c06072a8648ce3d0101022100fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f300604010004010704410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8022100fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141020101a144034200042af7a2aafe8dafd7dc7f9cfb58ce09bda7dce28653ab229b98d1d3d759660c672dd0db18c8c2d76aa470448e876fc2089ab1354c01a6e72cefc50915f4a963ee");
ECKey decodedKey = ECKey.fromASN1(privkeyASN1);
@ -135,7 +173,7 @@ public class ECKeyTest {
ECKey key = new ECKey();
String message = "Hello World!";
String signatureBase64 = key.signMessage(message);
System.out.println("Message signed with " + key.toAddress(NetworkParameters.prodNet()) + ": " + signatureBase64);
log.info("Message signed with " + key.toAddress(NetworkParameters.prodNet()) + ": " + signatureBase64);
// Should verify correctly.
key.verifyMessage(message, signatureBase64);
try {
@ -176,6 +214,146 @@ public class ECKeyTest {
}
@Test
public void testUnencryptedCreate() throws Exception {
ECKey unencryptedKey = new ECKey();
// The key should initially be unencrypted.
assertTrue(!unencryptedKey.isEncrypted());
// Copy the private key bytes for checking later.
byte[] originalPrivateKeyBytes = new byte[32];
System.arraycopy(unencryptedKey.getPrivKeyBytes(), 0, originalPrivateKeyBytes, 0, 32);
log.info("Original private key = " + Utils.bytesToHexString(originalPrivateKeyBytes));
// Encrypt the key.
ECKey encryptedKey = unencryptedKey.encrypt(keyCrypter, keyCrypter.deriveKey(PASSWORD1));
// The key should now be encrypted.
assertTrue("Key is not encrypted but it should be", encryptedKey.isEncrypted());
// The unencrypted private key bytes of the encrypted keychain
// should be null or all be blank.
byte[] privateKeyBytes = encryptedKey.getPrivKeyBytes();
if (privateKeyBytes != null) {
for (int i = 0; i < privateKeyBytes.length; i++) {
assertEquals("Byte " + i + " of the private key was not zero but should be", 0, privateKeyBytes[i]);
}
}
// Decrypt the key.
unencryptedKey = encryptedKey.decrypt(keyCrypter, keyCrypter.deriveKey(PASSWORD1));
// The key should be unencrypted
assertTrue("Key is not unencrypted but it should be", !unencryptedKey.isEncrypted());
// The reborn unencrypted private key bytes should match the
// original private key.
privateKeyBytes = unencryptedKey.getPrivKeyBytes();
log.info("Reborn decrypted private key = " + Utils.bytesToHexString(privateKeyBytes));
for (int i = 0; i < privateKeyBytes.length; i++) {
assertEquals("Byte " + i + " of the private key did not match the original", originalPrivateKeyBytes[i],
privateKeyBytes[i]);
}
}
@Test
public void testEncryptedCreate() throws Exception {
ECKey unencryptedKey = new ECKey();
// Copy the private key bytes for checking later.
byte[] originalPrivateKeyBytes = new byte[32];
System.arraycopy(unencryptedKey.getPrivKeyBytes(), 0, originalPrivateKeyBytes, 0, 32);
log.info("Original private key = " + Utils.bytesToHexString(originalPrivateKeyBytes));
EncryptedPrivateKey encryptedPrivateKey = keyCrypter.encrypt(unencryptedKey.getPrivKeyBytes(), keyCrypter.deriveKey(PASSWORD1));
ECKey encryptedKey = new ECKey(encryptedPrivateKey, unencryptedKey.getPubKey(), keyCrypter);
// The key should initially be encrypted
assertTrue("Key not encrypted at start", encryptedKey.isEncrypted());
// The unencrypted private key bytes of the encrypted keychain should all be blank.
byte[] privateKeyBytes = encryptedKey.getPrivKeyBytes();
if (privateKeyBytes != null) {
for (int i = 0; i < privateKeyBytes.length; i++) {
assertEquals("Byte " + i + " of the private key was not zero but should be", 0, privateKeyBytes[i]);
}
}
// Decrypt the key.
ECKey rebornUnencryptedKey = encryptedKey.decrypt(keyCrypter, keyCrypter.deriveKey(PASSWORD1));
// The key should be unencrypted
assertTrue("Key is not unencrypted but it should be", !rebornUnencryptedKey.isEncrypted());
// The reborn unencrypted private key bytes should match the original private key.
privateKeyBytes = rebornUnencryptedKey.getPrivKeyBytes();
log.info("Reborn decrypted private key = " + Utils.bytesToHexString(privateKeyBytes));
for (int i = 0; i < privateKeyBytes.length; i++) {
assertEquals("Byte " + i + " of the private key did not match the original", originalPrivateKeyBytes[i], privateKeyBytes[i]);
}
}
@Test
public void testEncryptionIsReversible() throws Exception {
ECKey originalUnencryptedKey = new ECKey();
EncryptedPrivateKey encryptedPrivateKey = keyCrypter.encrypt(originalUnencryptedKey.getPrivKeyBytes(), keyCrypter.deriveKey(PASSWORD1));
ECKey encryptedKey = new ECKey(encryptedPrivateKey, originalUnencryptedKey.getPubKey(), keyCrypter);
// The key should be encrypted
assertTrue("Key not encrypted at start", encryptedKey.isEncrypted());
// Check that the key can be successfully decrypted back to the original.
assertTrue("Key encryption is not reversible but it should be", ECKey.encryptionIsReversible(originalUnencryptedKey, encryptedKey, keyCrypter, keyCrypter.deriveKey(PASSWORD1)));
// Check that key encryption is not reversible if a password other than the original is used to generate the AES key.
assertTrue("Key encryption is reversible with wrong password", !ECKey.encryptionIsReversible(originalUnencryptedKey, encryptedKey, keyCrypter, keyCrypter.deriveKey(WRONG_PASSWORD)));
// Change one of the encrypted key bytes (this is to simulate a faulty keyCrypter).
// Encryption should not be reversible
byte[] goodEncryptedPrivateKeyBytes = encryptedPrivateKey.getEncryptedBytes();
// Break the encrypted private key and check it is broken.
byte[] badEncryptedPrivateKeyBytes = goodEncryptedPrivateKeyBytes;
// XOR the 16th byte with 0x0A (this is fairly arbitary) to break it.
badEncryptedPrivateKeyBytes[16] = (byte) (badEncryptedPrivateKeyBytes[12] ^ new Byte("12").byteValue());
encryptedPrivateKey.setEncryptedPrivateBytes(badEncryptedPrivateKeyBytes);
ECKey badEncryptedKey = new ECKey(encryptedPrivateKey, originalUnencryptedKey.getPubKey(), keyCrypter);
assertTrue("Key encryption is reversible with faulty encrypted bytes", !ECKey.encryptionIsReversible(originalUnencryptedKey, badEncryptedKey, keyCrypter, keyCrypter.deriveKey(PASSWORD1)));
}
@Test
public void testToString() throws Exception {
ECKey key = new ECKey(BigInteger.TEN); // An example private key.
assertEquals("pub:04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7", key.toString());
assertEquals("pub:04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7 priv:0a", key.toStringWithPrivate());
}
@Test
public void keyRecoveryWithEncryptedKey() throws Exception {
ECKey unencryptedKey = new ECKey();
KeyParameter aesKey = keyCrypter.deriveKey(PASSWORD1);
ECKey encryptedKey = unencryptedKey.encrypt(keyCrypter,aesKey);
String message = "Goodbye Jupiter!";
Sha256Hash hash = Sha256Hash.create(message.getBytes());
ECKey.ECDSASignature sig = encryptedKey.sign(hash, aesKey);
unencryptedKey = new ECKey(null, unencryptedKey.getPubKey());
boolean found = false;
for (int i = 0; i < 4; i++) {
ECKey key2 = ECKey.recoverFromSignature(i, sig, hash, true);
if (unencryptedKey.equals(key2)) {
found = true;
break;
}
}
assertTrue(found);
}
public void roundTripDumpedPrivKey() throws Exception {
ECKey key = new ECKey();
assertTrue(key.isCompressed());
@ -186,4 +364,51 @@ public class ECKeyTest {
assertTrue(Arrays.equals(key.getPrivKeyBytes(), key2.getPrivKeyBytes()));
assertTrue(Arrays.equals(key.getPubKey(), key2.getPubKey()));
}
@Test
public void clear() throws Exception {
ECKey unencryptedKey = new ECKey();
ECKey encryptedKey = (new ECKey()).encrypt(keyCrypter, keyCrypter.deriveKey(PASSWORD1));
checkSomeBytesAreNonZero(unencryptedKey.getPrivKeyBytes());
unencryptedKey.clearPrivateKey();
checkAllBytesAreZero(unencryptedKey.getPrivKeyBytes());
// The encryptedPrivateKey should be null in an unencrypted ECKey anyhow but check all the same.
assertTrue(unencryptedKey.getEncryptedPrivateKey() == null);
checkSomeBytesAreNonZero(encryptedKey.getPrivKeyBytes());
checkSomeBytesAreNonZero(encryptedKey.getEncryptedPrivateKey().getEncryptedBytes());
checkSomeBytesAreNonZero(encryptedKey.getEncryptedPrivateKey().getInitialisationVector());
encryptedKey.clearPrivateKey();
checkAllBytesAreZero(encryptedKey.getPrivKeyBytes());
checkAllBytesAreZero(encryptedKey.getEncryptedPrivateKey().getEncryptedBytes());
checkAllBytesAreZero(encryptedKey.getEncryptedPrivateKey().getInitialisationVector());
}
private boolean checkSomeBytesAreNonZero(byte[] bytes) {
if (bytes == null) {
return false;
} else {
for (int i = 0; i < bytes.length; i++) {
if (bytes[i] != 0) {
return true;
}
}
return false;
}
}
private boolean checkAllBytesAreZero(byte[] bytes) {
if (bytes == null) {
return true;
} else {
for (int i = 0; i < bytes.length; i++) {
if (bytes[i] != 0) {
return false;
}
}
return true;
}
}
}

View File

@ -48,7 +48,7 @@ public class FullBlockTestGenerator {
coinbaseOutKeyPubKey = coinbaseOutKey.getPubKey();
Utils.rollMockClock(0); // Set a mock clock for timestamp tests
}
public List<BlockAndValidity> getBlocksToTest(boolean addExpensiveBlocks) throws ScriptException, ProtocolException, IOException {
List<BlockAndValidity> blocks = new LinkedList<BlockAndValidity>();

View File

@ -18,20 +18,33 @@ package com.google.bitcoin.core;
import com.google.bitcoin.core.Transaction.SigHash;
import com.google.bitcoin.core.WalletTransaction.Pool;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.bitcoin.store.BlockStore;
import com.google.bitcoin.store.MemoryBlockStore;
import com.google.bitcoin.utils.BriefLogFormatter;
import com.google.bitcoin.utils.Locks;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.CycleDetectingLockFactory;
import com.google.protobuf.ByteString;
import org.bitcoinj.wallet.Protos;
import org.bitcoinj.wallet.Protos.ScryptParameters;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
@ -45,26 +58,74 @@ import static com.google.bitcoin.core.Utils.toNanoCoins;
import static org.junit.Assert.*;
public class WalletTest {
public Logger log = LoggerFactory.getLogger(WalletTest.class.getName());
static final NetworkParameters params = NetworkParameters.unitTests();
private Address myAddress;
private Address myEncryptedAddress;
private Address myEncryptedAddress2;
private Wallet wallet;
private Wallet encryptedWallet;
// A wallet with an initial unencrypted private key and an encrypted private key.
private Wallet encryptedHetergeneousWallet;
private BlockChain chain;
private BlockStore blockStore;
private ECKey myKey;
private ECKey myEncryptedKey;
private ECKey myKey2;
private ECKey myEncryptedKey2;
private static CharSequence PASSWORD1 = "my helicopter contains eels";
private static CharSequence WRONG_PASSWORD = "nothing noone nobody nowhere";
private KeyParameter aesKey;
private KeyParameter wrongAesKey;
private KeyCrypter keyCrypter;
private SecureRandom secureRandom = new SecureRandom();
@Before
public void setUp() throws Exception {
myKey = new ECKey();
myKey2 = new ECKey();
myAddress = myKey.toAddress(params);
wallet = new Wallet(params);
wallet.addKey(myKey);
byte[] salt = new byte[KeyCrypterScrypt.SALT_LENGTH];
secureRandom.nextBytes(salt);
Protos.ScryptParameters.Builder scryptParametersBuilder = Protos.ScryptParameters.newBuilder().setSalt(ByteString.copyFrom(salt));
ScryptParameters scryptParameters = scryptParametersBuilder.build();
keyCrypter = new KeyCrypterScrypt(scryptParameters);
wallet = new Wallet(params);
encryptedWallet = new Wallet(params, keyCrypter);
encryptedHetergeneousWallet = new Wallet(params, keyCrypter);
aesKey = keyCrypter.deriveKey(PASSWORD1);
wrongAesKey = keyCrypter.deriveKey(WRONG_PASSWORD);
wallet.addKey(myKey);
myEncryptedKey = encryptedWallet.addNewEncryptedKey(keyCrypter, aesKey);
myEncryptedAddress = myEncryptedKey.toAddress(params);
encryptedHetergeneousWallet.addKey(myKey2);
myEncryptedKey2 = encryptedHetergeneousWallet.addNewEncryptedKey(keyCrypter, aesKey);
myEncryptedAddress2 = myEncryptedKey2.toAddress(params);
blockStore = new MemoryBlockStore(params);
chain = new BlockChain(params, wallet, blockStore);
BriefLogFormatter.init();
}
private Transaction sendMoneyToWallet(Transaction tx, AbstractBlockChain.NewBlockType type)
private Transaction sendMoneyToWallet(Wallet wallet, Transaction tx, AbstractBlockChain.NewBlockType type)
throws IOException, ProtocolException, VerificationException {
if (type == null) {
// Pending/broadcast tx.
@ -79,53 +140,133 @@ public class WalletTest {
return tx;
}
private Transaction sendMoneyToWallet(BigInteger value, AbstractBlockChain.NewBlockType type)
private Transaction sendMoneyToWallet(Transaction tx, AbstractBlockChain.NewBlockType type) throws IOException,
ProtocolException, VerificationException {
return sendMoneyToWallet(this.wallet, tx, type);
}
private Transaction sendMoneyToWallet(Wallet wallet, BigInteger value, Address toAddress, AbstractBlockChain.NewBlockType type)
throws IOException, ProtocolException, VerificationException {
return sendMoneyToWallet(createFakeTx(params, value, myAddress), type);
return sendMoneyToWallet(wallet, createFakeTx(params, value, toAddress), type);
}
private Transaction sendMoneyToWallet(BigInteger value, AbstractBlockChain.NewBlockType type) throws IOException,
ProtocolException, VerificationException {
return sendMoneyToWallet(this.wallet, createFakeTx(params, value, myAddress), type);
}
@Test
public void basicSpending() throws Exception {
// We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change. We
// will attach a small fee. Because the Bitcoin protocol makes it difficult to determine the fee of an
// arbitrary transaction in isolation, we'll check that the fee was set by examining the size of the change.
basicSpendingCommon(wallet, myAddress, false);
}
@Test
public void basicSpendingWithEncryptedWallet() throws Exception {
basicSpendingCommon(encryptedWallet, myEncryptedAddress, true);
}
@Test
public void basicSpendingWithEncryptedHetergeneousWallet() throws Exception {
basicSpendingCommon(encryptedHetergeneousWallet, myEncryptedAddress2, true);
}
private void basicSpendingCommon(Wallet wallet, Address toAddress, boolean testEncryption) throws Exception {
// We'll set up a wallet that receives a coin, then sends a coin of
// lesser value and keeps the change. We
// will attach a small fee. Because the Bitcoin protocol makes it
// difficult to determine the fee of an
// arbitrary transaction in isolation, we'll check that the fee was set
// by examining the size of the change.
// Receive some money as a pending transaction.
BigInteger v1 = Utils.toNanoCoins(1, 0);
Transaction t1 = sendMoneyToWallet(v1, null);
assertEquals(BigInteger.ZERO, wallet.getBalance());
assertEquals(v1, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
assertEquals(1, wallet.getPoolSize(Pool.PENDING));
assertEquals(0, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
sendMoneyToWallet(t1, AbstractBlockChain.NewBlockType.BEST_CHAIN);
assertEquals(v1, wallet.getBalance());
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.ALL));
receiveAPendingTransaction(wallet, toAddress);
// Create a send with a fee.
// Prepare to send.
Address destination = new ECKey().toAddress(params);
BigInteger v2 = toNanoCoins(0, 50);
Wallet.SendRequest req = Wallet.SendRequest.to(destination, v2);
req.fee = toNanoCoins(0, 1);
if (testEncryption) {
// Try to create a send with a fee but no password (this should fail).
try {
wallet.completeTx(req);
fail("No exception was thrown trying to sign an encrypted key with no password supplied.");
} catch (KeyCrypterException kce) {
assertEquals("This ECKey is encrypted but no decryption key has been supplied.", kce.getMessage());
}
assertEquals("Wrong number of UNSPENT.1", 1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals("Wrong number of ALL.1", 1, wallet.getPoolSize(WalletTransaction.Pool.ALL));
// Try to create a send with a fee but the wrong password (this should fail).
req = Wallet.SendRequest.to(destination, v2);
req.aesKey = wrongAesKey;
req.fee = toNanoCoins(0, 1);
try {
wallet.completeTx(req);
fail("No exception was thrown trying to sign an encrypted key with the wrong password supplied.");
} catch (KeyCrypterException kce) {
assertEquals("Could not decrypt bytes", kce.getMessage());
}
assertEquals("Wrong number of UNSPENT.2", 1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals("Wrong number of ALL.2", 1, wallet.getPoolSize(WalletTransaction.Pool.ALL));
// Create a send with a fee with the correct password (this should succeed).
req = Wallet.SendRequest.to(destination, v2);
req.aesKey = aesKey;
req.fee = toNanoCoins(0, 1);
}
// Complete the transaction successfully.
wallet.completeTx(req);
Transaction t2 = req.tx;
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.ALL));
assertEquals("Wrong number of UNSPENT.3", 1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals("Wrong number of ALL.3", 1, wallet.getPoolSize(WalletTransaction.Pool.ALL));
assertEquals(TransactionConfidence.Source.SELF, t2.getConfidence().getSource());
assertEquals(wallet.getChangeAddress(), t2.getOutput(1).getScriptPubKey().getToAddress());
// Do some basic sanity checks.
assertEquals(1, t2.getInputs().size());
assertEquals(myAddress, t2.getInputs().get(0).getScriptSig().getFromAddress());
assertEquals(t2.getConfidence().getConfidenceType(), TransactionConfidence.ConfidenceType.NOT_SEEN_IN_CHAIN);
assertEquals(2, t2.getOutputs().size());
assertEquals(destination, t2.getOutputs().get(0).getScriptPubKey().getToAddress());
assertEquals(wallet.getChangeAddress(), t2.getOutputs().get(1).getScriptPubKey().getToAddress());
BigInteger v3 = toNanoCoins(0, 49);
assertEquals(v3, t2.getOutputs().get(1).getValue());
// Check the script runs and signatures verify.
t2.getInputs().get(0).verify();
basicSanityChecks(wallet, t2, toAddress, destination);
// Broadcast the transaction and commit.
broadcastAndCommit(wallet, t2);
// Now check that we can spend the unconfirmed change, with a new change
// address of our own selection.
// (req.aesKey is null for unencrypted / the correct aesKey for encrypted.)
spendUnconfirmedChange(wallet, t2, req.aesKey);
}
private void receiveAPendingTransaction(Wallet wallet, Address toAddress) throws Exception {
BigInteger v1 = Utils.toNanoCoins(1, 0);
Transaction t1 = sendMoneyToWallet(wallet, v1, toAddress, null);
assertEquals(BigInteger.ZERO, wallet.getBalance());
assertEquals(v1, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
assertEquals(1, wallet.getPoolSize(Pool.PENDING));
assertEquals(0, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
sendMoneyToWallet(wallet, t1, AbstractBlockChain.NewBlockType.BEST_CHAIN);
assertEquals("Incorrect confirmed tx balance", v1, wallet.getBalance());
assertEquals("Incorrect confirmed tx PENDING pool size", 0, wallet.getPoolSize(WalletTransaction.Pool.PENDING));
assertEquals("Incorrect confirmed tx UNSPENT pool size", 1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals("Incorrect confirmed tx ALL pool size", 1, wallet.getPoolSize(WalletTransaction.Pool.ALL));
}
private void basicSanityChecks(Wallet wallet, Transaction t, Address fromAddress, Address destination) throws ScriptException {
assertEquals("Wrong number of tx inputs", 1, t.getInputs().size());
assertEquals(fromAddress, t.getInputs().get(0).getScriptSig().getFromAddress());
assertEquals(t.getConfidence().getConfidenceType(), TransactionConfidence.ConfidenceType.NOT_SEEN_IN_CHAIN);
assertEquals("Wrong number of tx outputs",2, t.getOutputs().size());
assertEquals(destination, t.getOutputs().get(0).getScriptPubKey().getToAddress());
assertEquals(wallet.getChangeAddress(), t.getOutputs().get(1).getScriptPubKey().getToAddress());
assertEquals(toNanoCoins(0, 49), t.getOutputs().get(1).getValue());
// Check the script runs and signatures verify.
t.getInputs().get(0).verify();
}
private void broadcastAndCommit(Wallet wallet, Transaction t) throws Exception {
final LinkedList<Transaction> txns = Lists.newLinkedList();
wallet.addEventListener(new AbstractWalletEventListener() {
@Override
@ -133,19 +274,22 @@ public class WalletTest {
txns.add(tx);
}
});
// We broadcast the TX over the network, and then commit to it.
t2.getConfidence().markBroadcastBy(new PeerAddress(InetAddress.getByAddress(new byte[]{1,2,3,4})));
t2.getConfidence().markBroadcastBy(new PeerAddress(InetAddress.getByAddress(new byte[]{10,2,3,4})));
wallet.commitTx(t2);
t.getConfidence().markBroadcastBy(new PeerAddress(InetAddress.getByAddress(new byte[]{1,2,3,4})));
t.getConfidence().markBroadcastBy(new PeerAddress(InetAddress.getByAddress(new byte[]{10,2,3,4})));
wallet.commitTx(t);
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.PENDING));
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.SPENT));
assertEquals(2, wallet.getPoolSize(WalletTransaction.Pool.ALL));
assertEquals(t2, txns.getFirst());
assertEquals(t, txns.getFirst());
assertEquals(1, txns.size());
}
// Now check that we can spend the unconfirmed change, with a new change address of our own selection.
private void spendUnconfirmedChange(Wallet wallet, Transaction t2, KeyParameter aesKey) throws Exception {
BigInteger v3 = toNanoCoins(0, 49);
assertEquals(v3, wallet.getBalance());
req = Wallet.SendRequest.to(new ECKey().toAddress(params), toNanoCoins(0, 48));
Wallet.SendRequest req = Wallet.SendRequest.to(new ECKey().toAddress(params), toNanoCoins(0, 48));
req.aesKey = aesKey;
Address a = req.changeAddress = new ECKey().toAddress(params);
wallet.completeTx(req);
Transaction t3 = req.tx;
@ -153,7 +297,7 @@ public class WalletTest {
assertNotNull(t3);
wallet.commitTx(t3);
assertTrue(wallet.isConsistent());
// t2 and t3 gets confirmed in the same block.
// t2 and t3 gets confirmed in the same block.
BlockPair bp = createFakeBlock(blockStore, t2, t3);
wallet.receiveFromBlock(t2, bp.storedBlock, AbstractBlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(t3, bp.storedBlock, AbstractBlockChain.NewBlockType.BEST_CHAIN);
@ -340,12 +484,12 @@ public class WalletTest {
tx.addOutput(output);
wallet.receiveFromBlock(tx, null, BlockChain.NewBlockType.BEST_CHAIN);
assertTrue(wallet.isConsistent());
assertTrue("Wallet is not consistent", wallet.isConsistent());
Transaction txClone = new Transaction(params, tx.bitcoinSerialize());
try {
wallet.receiveFromBlock(txClone, null, BlockChain.NewBlockType.BEST_CHAIN);
fail();
fail("Illegal argument not thrown when it should have been.");
} catch (IllegalStateException ex) {
// expected
}
@ -751,7 +895,7 @@ public class WalletTest {
// TODO: This code is messy, improve the Script class and fixinate!
assertEquals(t2.toString(), 1, t2.getInputs().get(0).getScriptSig().chunks.size());
assertTrue(t2.getInputs().get(0).getScriptSig().chunks.get(0).data.length > 50);
System.out.println(t2);
log.info(t2.toString(chain));
}
@Test
@ -764,23 +908,23 @@ public class WalletTest {
ECKey key = new ECKey();
wallet.addKey(key);
Sha256Hash hash2 = Sha256Hash.hashFileContents(f);
assertFalse(hash1.equals(hash2)); // File has changed.
assertFalse("Wallet not saved after addKey", hash1.equals(hash2)); // File has changed.
Transaction t1 = createFakeTx(params, toNanoCoins(5, 0), key);
if (wallet.isPendingTransactionRelevant(t1))
wallet.receivePending(t1, null);
Sha256Hash hash3 = Sha256Hash.hashFileContents(f);
assertFalse(hash2.equals(hash3)); // File has changed again.
assertFalse("Wallet not saved after receivePending", hash2.equals(hash3)); // File has changed again.
Block b1 = createFakeBlock(blockStore, t1).block;
chain.add(b1);
Sha256Hash hash4 = Sha256Hash.hashFileContents(f);
assertFalse(hash3.equals(hash4)); // File has changed again.
assertFalse("Wallet not saved after chain add.1", hash3.equals(hash4)); // File has changed again.
// Check that receiving some block without any relevant transactions still triggers a save.
Block b2 = b1.createNextBlock(new ECKey().toAddress(params));
chain.add(b2);
assertFalse(hash4.equals(Sha256Hash.hashFileContents(f))); // File has changed again.
assertFalse("Wallet not saved after chain add.2", hash4.equals(Sha256Hash.hashFileContents(f))); // File has changed again.
}
@Test
@ -846,7 +990,7 @@ public class WalletTest {
@Test
public void spendOutputFromPendingTransaction() throws Exception {
// We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change.
// We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change.
BigInteger v1 = Utils.toNanoCoins(1, 0);
sendMoneyToWallet(v1, AbstractBlockChain.NewBlockType.BEST_CHAIN);
// First create our current transaction
@ -884,6 +1028,127 @@ public class WalletTest {
}
@Test
public void encryptionDecryptionBasic() throws Exception {
encryptionDecryptionBasicCommon(encryptedWallet);
encryptionDecryptionBasicCommon(encryptedHetergeneousWallet);
}
private void encryptionDecryptionBasicCommon(Wallet wallet) {
// Check the wallet is initially of WalletType ENCRYPTED.
assertTrue("Wallet is not an encrypted wallet", wallet.getEncryptionType() == EncryptionType.ENCRYPTED_SCRYPT_AES);
// Correct password should decrypt first encrypted private key.
assertTrue("checkPasswordCanDecryptFirstPrivateKey result is wrong with correct password.2", wallet.checkPassword(PASSWORD1));
// Incorrect password should not decrypt first encrypted private key.
assertTrue("checkPasswordCanDecryptFirstPrivateKey result is wrong with incorrect password.3", !wallet.checkPassword(WRONG_PASSWORD));
// Decrypt wallet.
assertTrue("The keyCrypter is missing but should not be", keyCrypter != null);
wallet.decrypt(aesKey);
// Wallet should now be unencrypted.
assertTrue("Wallet is not an unencrypted wallet", wallet.getKeyCrypter() == null);
// Correct password should not decrypt first encrypted private key as wallet is unencrypted.
assertTrue("checkPasswordCanDecryptFirstPrivateKey result is wrong with correct password", !wallet.checkPassword(PASSWORD1));
// Incorrect password should not decrypt first encrypted private key as wallet is unencrypted.
assertTrue("checkPasswordCanDecryptFirstPrivateKey result is wrong with incorrect password", !wallet.checkPassword(WRONG_PASSWORD));
// Encrypt wallet.
wallet.encrypt(keyCrypter, aesKey);
// Wallet should now be of type WalletType.ENCRYPTED_SCRYPT_AES.
assertTrue("Wallet is not an encrypted wallet", wallet.getEncryptionType() == EncryptionType.ENCRYPTED_SCRYPT_AES);
}
@Test
public void encryptionDecryptionBadPassword() throws Exception {
// Check the wallet is currently encrypted
assertTrue("Wallet is not an encrypted wallet", encryptedWallet.getEncryptionType() == EncryptionType.ENCRYPTED_SCRYPT_AES);
// Chek that the wrong password does not decrypt the wallet.
try {
encryptedWallet.decrypt(wrongAesKey);
fail("Incorrectly decoded wallet with wrong password");
} catch (KeyCrypterException ede) {
assertTrue("Wrong message in EncrypterDecrypterException", ede.getMessage().indexOf("Could not decrypt bytes") > -1);
}
}
@Test
public void encryptionDecryptionCheckExceptions() throws Exception {
// Check the wallet is currently encrypted
assertTrue("Wallet is not an encrypted wallet", encryptedWallet.getEncryptionType() == EncryptionType.ENCRYPTED_SCRYPT_AES);
// Decrypt wallet.
assertTrue("The keyCrypter is missing but should not be.1", keyCrypter != null);
encryptedWallet.decrypt(aesKey);
// Try decrypting it again
try {
assertTrue("The keyCrypter is missing but should not be.2", keyCrypter != null);
encryptedWallet.decrypt(aesKey);
fail("Should not be able to decrypt a decrypted wallet");
} catch (IllegalStateException e) {
assertTrue("Expected behaviour", true);
}
assertTrue("Wallet is not an unencrypted wallet.2", encryptedWallet.getKeyCrypter() == null);
// Encrypt wallet.
encryptedWallet.encrypt(keyCrypter, aesKey);
assertTrue("Wallet is not an encrypted wallet.2", encryptedWallet.getEncryptionType() == EncryptionType.ENCRYPTED_SCRYPT_AES);
// Try encrypting it again
try {
encryptedWallet.encrypt(keyCrypter, aesKey);
fail("Should not be able to encrypt an encrypted wallet");
} catch (IllegalStateException e) {
assertTrue("Expected behaviour", true);
}
assertTrue("Wallet is not an encrypted wallet.3", encryptedWallet.getEncryptionType() == EncryptionType.ENCRYPTED_SCRYPT_AES);
}
@Test
public void encryptionDecryptionHomogenousKeys() throws Exception {
// Check the wallet is currently encrypted
assertTrue("Wallet is not an encrypted wallet", encryptedWallet.getEncryptionType() == EncryptionType.ENCRYPTED_SCRYPT_AES);
// Try added an ECKey that was encrypted with a differenct ScryptParameters (i.e. a non-homogenous key).
// This is not allowed as the ScryptParameters is stored at the Wallet level.
byte[] salt = new byte[KeyCrypterScrypt.SALT_LENGTH];
secureRandom.nextBytes(salt);
Protos.ScryptParameters.Builder scryptParametersBuilder = Protos.ScryptParameters.newBuilder().setSalt(ByteString.copyFrom(salt));
ScryptParameters scryptParameters = scryptParametersBuilder.build();
KeyCrypter keyCrypterDifferent = new KeyCrypterScrypt(scryptParameters);
ECKey ecKeyDifferent = new ECKey();
ecKeyDifferent = ecKeyDifferent.encrypt(keyCrypterDifferent, aesKey);
Iterable<ECKey> keys = encryptedWallet.getKeys();
Iterator iterator = keys.iterator();
boolean oneKey = iterator.hasNext();
iterator.next();
assertTrue("Wrong number of keys in wallet before key addition", oneKey && !iterator.hasNext());
try {
encryptedWallet.addKey(ecKeyDifferent);
fail("AddKey should have thrown an EncrypterDecrypterException but did not.");
} catch (KeyCrypterException ede) {
// Expected behaviour.
}
keys = encryptedWallet.getKeys();
iterator = keys.iterator();
oneKey = iterator.hasNext();
iterator.next();
assertTrue("Wrong number of keys in wallet after key addition", oneKey && !iterator.hasNext());
}
public void ageMattersDuringSelection() throws Exception {
// Test that we prefer older coins to newer coins when building spends. This reduces required fees and improves
// time to confirmation as the transaction will appear less spammy.

View File

@ -0,0 +1,163 @@
/**
* Copyright 2013 Jim Burton.
*
* Licensed under the MIT license (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://opensource.org/licenses/mit-license.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.crypto;
import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;
import java.util.Random;
import java.util.UUID;
import junit.framework.TestCase;
import org.bitcoinj.wallet.Protos;
import org.bitcoinj.wallet.Protos.ScryptParameters;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.bitcoin.core.Utils;
import com.google.bitcoin.utils.BriefLogFormatter;
import com.google.protobuf.ByteString;
public class KeyCrypterScryptTest extends TestCase {
private static final Logger log = LoggerFactory.getLogger(KeyCrypterScryptTest.class);
// Nonsense bytes for encryption test.
private static final byte[] TEST_BYTES1 = new byte[]{0, -101, 2, 103, -4, 105, 6, 107, 8, -109, 10, 111, -12, 113, 14, -115, 16, 117, -18, 119, 20, 121, 22, 123, -24, 125, 26, 127, -28, 29, -30, 31};
private static CharSequence PASSWORD1 = "aTestPassword";
private static CharSequence PASSWORD2 = "0123456789";
private static CharSequence WRONG_PASSWORD = "thisIsTheWrongPassword";
private SecureRandom secureRandom;
private ScryptParameters scryptParameters;
@Before
public void setUp() throws Exception {
secureRandom = new SecureRandom();
byte[] salt = new byte[KeyCrypterScrypt.SALT_LENGTH];
secureRandom.nextBytes(salt);
Protos.ScryptParameters.Builder scryptParametersBuilder = Protos.ScryptParameters.newBuilder().setSalt(ByteString.copyFrom(salt));
scryptParameters = scryptParametersBuilder.build();
BriefLogFormatter.init();
}
@Test
public void testKeyCrypterGood1() throws KeyCrypterException {
KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(scryptParameters);
// Encrypt.
EncryptedPrivateKey encryptedPrivateKey = keyCrypter.encrypt(TEST_BYTES1, keyCrypter.deriveKey(PASSWORD1));
assertNotNull(encryptedPrivateKey);
// Decrypt.
byte[] reborn = keyCrypter.decrypt(encryptedPrivateKey, keyCrypter.deriveKey(PASSWORD1));
log.debug("Original: " + Utils.bytesToHexString(TEST_BYTES1));
log.debug("Reborn : " + Utils.bytesToHexString(reborn));
assertEquals(Utils.bytesToHexString(TEST_BYTES1), Utils.bytesToHexString(reborn));
}
/**
* Test with random plain text strings and random passwords.
* UUIDs are used and hence will only cover hex characters (and the separator hyphen).
* @throws KeyCrypterException
* @throws UnsupportedEncodingException
*/
public void testKeyCrypterGood2() throws KeyCrypterException, UnsupportedEncodingException {
KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(scryptParameters);
int numberOfTests = 16;
System.out.print("EncrypterDecrypterTest: Trying random UUIDs for plainText and passwords :");
for (int i = 0; i < numberOfTests; i++) {
// Create a UUID as the plaintext and use another for the password.
String plainText = UUID.randomUUID().toString();
CharSequence password = UUID.randomUUID().toString();
EncryptedPrivateKey encryptedPrivateKey = keyCrypter.encrypt(plainText.getBytes(), keyCrypter.deriveKey(password));
assertNotNull(encryptedPrivateKey);
byte[] reconstructedPlainBytes = keyCrypter.decrypt(encryptedPrivateKey,keyCrypter.deriveKey(password));
assertEquals(Utils.bytesToHexString(plainText.getBytes()), Utils.bytesToHexString(reconstructedPlainBytes));
System.out.print('.');
}
System.out.println(" Done.");
}
public void testKeyCrypterWrongPassword() throws KeyCrypterException {
KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(scryptParameters);
// create a longer encryption string
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < 100; i++) {
stringBuffer.append(i + " ").append("The quick brown fox");
}
EncryptedPrivateKey encryptedPrivateKey = keyCrypter.encrypt(stringBuffer.toString().getBytes(), keyCrypter.deriveKey(PASSWORD2));
assertNotNull(encryptedPrivateKey);
try {
keyCrypter.decrypt(encryptedPrivateKey, keyCrypter.deriveKey(WRONG_PASSWORD));
fail("Decrypt with wrong password did not throw exception");
} catch (KeyCrypterException ede) {
assertTrue(ede.getMessage().indexOf("Could not decrypt") > -1);
}
}
@Test
public void testEncryptDecryptBytes1() throws KeyCrypterException {
KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(scryptParameters);
// Encrypt bytes.
EncryptedPrivateKey encryptedPrivateKey = keyCrypter.encrypt(TEST_BYTES1, keyCrypter.deriveKey(PASSWORD1));
assertNotNull(encryptedPrivateKey);
log.debug("\nEncrypterDecrypterTest: cipherBytes = \nlength = " + encryptedPrivateKey.getEncryptedBytes().length + "\n---------------\n" + Utils.bytesToHexString(encryptedPrivateKey.getEncryptedBytes()) + "\n---------------\n");
byte[] rebornPlainBytes = keyCrypter.decrypt(encryptedPrivateKey, keyCrypter.deriveKey(PASSWORD1));
log.debug("Original: " + Utils.bytesToHexString(TEST_BYTES1));
log.debug("Reborn1 : " + Utils.bytesToHexString(rebornPlainBytes));
assertEquals( Utils.bytesToHexString(TEST_BYTES1), Utils.bytesToHexString(rebornPlainBytes));
}
@Test
public void testEncryptDecryptBytes2() throws KeyCrypterException {
KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(scryptParameters);
// Encrypt random bytes of various lengths up to length 50.
Random random = new Random();
for (int i = 0; i < 50; i++) {
byte[] plainBytes = new byte[i];
random.nextBytes(plainBytes);
EncryptedPrivateKey encryptedPrivateKey = keyCrypter.encrypt(plainBytes, keyCrypter.deriveKey(PASSWORD1));
assertNotNull(encryptedPrivateKey);
//log.debug("\nEncrypterDecrypterTest: cipherBytes = \nlength = " + cipherBytes.length + "\n---------------\n" + Utils.bytesToHexString(cipherBytes) + "\n---------------\n");
byte[] rebornPlainBytes = keyCrypter.decrypt(encryptedPrivateKey, keyCrypter.deriveKey(PASSWORD1));
log.debug("Original: (" + i + ") " + Utils.bytesToHexString(plainBytes));
log.debug("Reborn1 : (" + i + ") " + Utils.bytesToHexString(rebornPlainBytes));
assertEquals( Utils.bytesToHexString(plainBytes), Utils.bytesToHexString(rebornPlainBytes));
}
}
}

View File

@ -6,6 +6,7 @@ import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
import com.google.bitcoin.utils.BriefLogFormatter;
import com.google.protobuf.ByteString;
import org.bitcoinj.wallet.Protos;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.junit.Before;
import org.junit.Test;
@ -19,12 +20,16 @@ import java.util.*;
import static com.google.bitcoin.core.TestUtils.createFakeTx;
import static org.junit.Assert.*;
import com.google.bitcoin.crypto.KeyCrypter;
public class WalletProtobufSerializerTest {
static final NetworkParameters params = NetworkParameters.unitTests();
private ECKey myKey;
private Address myAddress;
private Wallet myWallet;
public static String WALLET_DESCRIPTION = "The quick brown fox lives in \u4f26\u6566"; // Beijing in Chinese
@Before
public void setUp() throws Exception {
BriefLogFormatter.initVerbose();
@ -33,6 +38,7 @@ public class WalletProtobufSerializerTest {
myAddress = myKey.toAddress(params);
myWallet = new Wallet(params);
myWallet.addKey(myKey);
myWallet.setDescription(WALLET_DESCRIPTION);
}
@Test
@ -47,6 +53,7 @@ public class WalletProtobufSerializerTest {
wallet1.findKeyFromPubHash(myKey.getPubKeyHash()).getPrivKeyBytes());
assertEquals(myKey.getCreationTimeSeconds(),
wallet1.findKeyFromPubHash(myKey.getPubKeyHash()).getCreationTimeSeconds());
assertEquals(WALLET_DESCRIPTION, wallet1.getDescription());
}
@Test
@ -220,7 +227,7 @@ public class WalletProtobufSerializerTest {
assertEquals(work2, rebornConfidence1.getWorkDone());
}
private Wallet roundTrip(Wallet wallet) throws IOException {
private Wallet roundTrip(Wallet wallet) throws Exception {
ByteArrayOutputStream output = new ByteArrayOutputStream();
//System.out.println(WalletProtobufSerializer.walletToText(wallet));
new WalletProtobufSerializer().writeWallet(wallet, output);
@ -252,14 +259,14 @@ public class WalletProtobufSerializerTest {
rnd.nextBytes(wallet1.random_bytes);
Wallet wallet2 = roundTripExtension(wallet1);
assertTrue(wallet2 instanceof WalletExtension);
assertTrue("Wallet2 is not an instance of WalletExtension. It is a " + wallet2.getClass().getCanonicalName(), wallet2 instanceof WalletExtension);
WalletExtension wallet2ext = (WalletExtension)wallet2;
assertNotNull(wallet2ext.random_bytes);
assertNotNull("Wallet2s random bytes were null", wallet2ext.random_bytes);
for (int i = 0; i < 100; i++) {
assertEquals(wallet1.random_bytes[i], wallet2ext.random_bytes[i]);
assertEquals("Wallet extension byte different at byte " + i, wallet1.random_bytes[i], wallet2ext.random_bytes[i]);
}
}
@ -277,7 +284,7 @@ public class WalletProtobufSerializerTest {
}
private Wallet roundTripExtension(Wallet wallet) throws IOException {
private Wallet roundTripExtension(Wallet wallet) throws Exception {
ByteArrayOutputStream output = new ByteArrayOutputStream();
WalletProtobufSerializer serializer = new WalletProtobufSerializer();
@ -320,7 +327,13 @@ public class WalletProtobufSerializerTest {
public Wallet newWallet(NetworkParameters params) {
return new WalletExtension(params);
}
@Override
public Wallet newWallet(NetworkParameters params, KeyCrypter keyCrypter) {
// Ignore encryption.
return new WalletExtension(params);
}
@Override
public void readExtension(Wallet wallet, Protos.Extension extProto) {
if (wallet instanceof WalletExtension) {

View File

@ -288,7 +288,7 @@ public class PeerMonitor {
if (column == PeerTableModel.CHAIN_HEIGHT) {
long height = (Long) contents;
if (height != peerGroup.getMostCommonChainHeight()) {
str = height + " ";
str = height + " \u2022 ";
}
}
}

View File

@ -17,6 +17,7 @@
package com.google.bitcoin.examples;
import com.google.bitcoin.core.*;
import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.discovery.DnsDiscovery;
import com.google.bitcoin.discovery.IrcDiscovery;
import com.google.bitcoin.store.BlockStore;
@ -188,6 +189,9 @@ public class PingService {
} catch (ScriptException e) {
// If we didn't understand the scriptSig, just crash.
throw new RuntimeException(e);
} catch (KeyCrypterException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}

View File

@ -189,6 +189,13 @@
<artifactId>protobuf-java</artifactId>
<version>${protobuf.version}</version>
</dependency>
<dependency>
<groupId>com.lambdaworks</groupId>
<artifactId>scrypt</artifactId>
<version>1.3.3</version>
</dependency>
</dependencies>
</dependencyManagement>
@ -206,5 +213,4 @@
<netty.version>3.6.3.Final</netty.version>
<generated.sourceDirectory>gen</generated.sourceDirectory>
</properties>
</project>

View File

@ -17,6 +17,7 @@
package com.google.bitcoin.tools;
import com.google.bitcoin.core.*;
import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.discovery.DnsDiscovery;
import com.google.bitcoin.discovery.IrcDiscovery;
import com.google.bitcoin.discovery.PeerDiscovery;
@ -432,6 +433,8 @@ public class WalletTool {
return;
} catch (ScriptException e) {
throw new RuntimeException(e);
} catch (KeyCrypterException e) {
throw new RuntimeException(e);
}
t = req.tx; // Not strictly required today.
setup();
@ -443,6 +446,8 @@ public class WalletTool {
System.out.println(t.getHashAsString());
} catch (BlockStoreException e) {
throw new RuntimeException(e);
} catch (KeyCrypterException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
@ -666,7 +671,11 @@ public class WalletTool {
System.err.println("That key already exists in this wallet.");
return;
}
wallet.addKey(key);
try {
wallet.addKey(key);
} catch (KeyCrypterException kce) {
System.err.println("There was an encryption related error when adding the key. The error was '" + kce.getMessage() + "'.");
}
System.out.println("addr:" + key.toAddress(params) + " " + key);
}