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:
parent
17efb4d6b7
commit
5d0518dafd
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
68
core/src/main/java/com/google/bitcoin/crypto/KeyCrypter.java
Normal file
68
core/src/main/java/com/google/bitcoin/crypto/KeyCrypter.java
Normal 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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
|
@ -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
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>();
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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 ";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
8
pom.xml
8
pom.xml
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user