All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.bitcoinj.wallet.BasicKeyChain Maven / Gradle / Ivy

There is a newer version: 0.17-beta1
Show newest version
/*
 * Copyright 2013 Google Inc.
 * Copyright 2019 Andreas Schildbach
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 org.bitcoinj.wallet;

import com.google.protobuf.ByteString;
import org.bitcoinj.crypto.AesKey;
import org.bitcoinj.core.BloomFilter;
import org.bitcoinj.crypto.ECKey;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.crypto.EncryptableItem;
import org.bitcoinj.crypto.EncryptedData;
import org.bitcoinj.crypto.KeyCrypter;
import org.bitcoinj.crypto.KeyCrypterException;
import org.bitcoinj.crypto.KeyCrypterScrypt;
import org.bitcoinj.utils.ListenerRegistration;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.listeners.KeyChainEventListener;

import javax.annotation.Nullable;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

import static org.bitcoinj.base.internal.Preconditions.checkArgument;
import static org.bitcoinj.base.internal.Preconditions.checkState;

/**
 * A {@link KeyChain} that implements the simplest model possible: it can have keys imported into it, and just acts as
 * a dumb bag of keys. It will, left to its own devices, always return the same key for usage by the wallet, although
 * it will automatically add one to itself if it's empty or if encryption is requested.
 */
public class BasicKeyChain implements EncryptableKeyChain {
    private final ReentrantLock lock = Threading.lock(BasicKeyChain.class);

    // Maps used to let us quickly look up a key given data we find in transactions or the block chain.
    private final LinkedHashMap hashToKeys;
    private final LinkedHashMap pubkeyToKeys;
    @Nullable private final KeyCrypter keyCrypter;
    private boolean isWatching;

    private final CopyOnWriteArrayList> listeners;

    public BasicKeyChain() {
        this(null);
    }

    public BasicKeyChain(@Nullable KeyCrypter crypter) {
        this.keyCrypter = crypter;
        hashToKeys = new LinkedHashMap<>();
        pubkeyToKeys = new LinkedHashMap<>();
        listeners = new CopyOnWriteArrayList<>();
    }

    /** Returns the {@link KeyCrypter} in use or null if the key chain is not encrypted. */
    @Override
    @Nullable
    public KeyCrypter getKeyCrypter() {
        lock.lock();
        try {
            return keyCrypter;
        } finally {
            lock.unlock();
        }
    }

    @Override
    public ECKey getKey(@Nullable KeyPurpose ignored) {
        lock.lock();
        try {
            if (hashToKeys.isEmpty()) {
                checkState(keyCrypter == null);   // We will refuse to encrypt an empty key chain.
                final ECKey key = new ECKey();
                importKeyLocked(key);
                queueOnKeysAdded(Collections.singletonList(key));
            }
            return hashToKeys.values().iterator().next();
        } finally {
            lock.unlock();
        }
    }

    @Override
    public List getKeys(@Nullable KeyPurpose purpose, int numberOfKeys) {
        checkArgument(numberOfKeys > 0);
        lock.lock();
        try {
            if (hashToKeys.size() < numberOfKeys) {
                checkState(keyCrypter == null);

                List keys = new ArrayList<>();
                for (int i = 0; i < numberOfKeys - hashToKeys.size(); i++) {
                    keys.add(new ECKey());
                }

                List immutableKeys = Collections.unmodifiableList(keys);
                importKeysLocked(immutableKeys);
                queueOnKeysAdded(immutableKeys);
            }

            List keysToReturn = new ArrayList<>();
            int count = 0;
            while (hashToKeys.values().iterator().hasNext() && numberOfKeys != count) {
                keysToReturn.add(hashToKeys.values().iterator().next());
                count++;
            }
            return keysToReturn;
        } finally {
            lock.unlock();
        }
    }

    /** Returns a copy of the list of keys that this chain is managing. */
    public List getKeys() {
        lock.lock();
        try {
            return new ArrayList<>(hashToKeys.values());
        } finally {
            lock.unlock();
        }
    }

    public int importKeys(ECKey... keys) {
        return importKeys(Collections.unmodifiableList(Arrays.asList(keys)));
    }

    public int importKeys(List keys) {
        lock.lock();
        try {
            // Check that if we're encrypted, the keys are all encrypted, and if we're not, that none are.
            // We are NOT checking that the actual password matches here because we don't have access to the password at
            // this point: if you screw up and import keys with mismatched passwords, you lose! So make sure the
            // password is checked first.
            for (ECKey key : keys) {
                checkKeyEncryptionStateMatches(key);
            }
            List actuallyAdded = new ArrayList<>(keys.size());
            for (final ECKey key : keys) {
                if (hasKey(key)) continue;
                actuallyAdded.add(key);
                importKeyLocked(key);
            }
            if (actuallyAdded.size() > 0)
                queueOnKeysAdded(actuallyAdded);
            return actuallyAdded.size();
        } finally {
            lock.unlock();
        }
    }

    private void checkKeyEncryptionStateMatches(ECKey key) {
        if (keyCrypter == null && key.isEncrypted())
            throw new KeyCrypterException("Key is encrypted but chain is not");
        else if (keyCrypter != null && !key.isEncrypted())
            throw new KeyCrypterException("Key is not encrypted but chain is");
        else if (keyCrypter != null && key.getKeyCrypter() != null && !key.getKeyCrypter().equals(keyCrypter))
            throw new KeyCrypterException("Key encrypted under different parameters to chain");
    }

    private void importKeyLocked(ECKey key) {
        if (hashToKeys.isEmpty()) {
            isWatching = key.isWatching();
        } else {
            if (key.isWatching() && !isWatching)
                throw new IllegalArgumentException("Key is watching but chain is not");
            if (!key.isWatching() && isWatching)
                throw new IllegalArgumentException("Key is not watching but chain is");
        }
        ECKey previousKey = pubkeyToKeys.put(ByteString.copyFrom(key.getPubKey()), key);
        hashToKeys.put(ByteString.copyFrom(key.getPubKeyHash()), key);
        checkState(previousKey == null);
    }

    private void importKeysLocked(List keys) {
        for (ECKey key : keys) {
            importKeyLocked(key);
        }
    }

    /**
     * Imports a key to the key chain. If key is present in the key chain, ignore it.
     */
    public void importKey(ECKey key) {
        lock.lock();
        try {
            checkKeyEncryptionStateMatches(key);
            if (hasKey(key)) return;
            importKeyLocked(key);
            queueOnKeysAdded(Collections.singletonList(key));
        } finally {
            lock.unlock();
        }
    }

    public ECKey findKeyFromPubHash(byte[] pubKeyHash) {
        lock.lock();
        try {
            return hashToKeys.get(ByteString.copyFrom(pubKeyHash));
        } finally {
            lock.unlock();
        }
    }

    public ECKey findKeyFromPubKey(byte[] pubKey) {
        lock.lock();
        try {
            return pubkeyToKeys.get(ByteString.copyFrom(pubKey));
        } finally {
            lock.unlock();
        }
    }

    @Override
    public boolean hasKey(ECKey key) {
        return findKeyFromPubKey(key.getPubKey()) != null;
    }

    @Override
    public int numKeys() {
        return pubkeyToKeys.size();
    }

    /** Whether this basic key chain is empty, full of regular (usable for signing) keys, or full of watching keys. */
    public enum State {
        EMPTY,
        WATCHING,
        REGULAR
    }

    /**
     * Returns whether this chain consists of pubkey only (watching) keys, regular keys (usable for signing), or
     * has no keys in it yet at all (thus we cannot tell).
     */
    public State isWatching() {
        lock.lock();
        try {
            if (hashToKeys.isEmpty())
                return State.EMPTY;
            return isWatching ? State.WATCHING : State.REGULAR;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Removes the given key from the keychain. Be very careful with this - losing a private key destroys the
     * money associated with it.
     * @return Whether the key was removed or not.
     */
    public boolean removeKey(ECKey key) {
        lock.lock();
        try {
            boolean a = hashToKeys.remove(ByteString.copyFrom(key.getPubKeyHash())) != null;
            boolean b = pubkeyToKeys.remove(ByteString.copyFrom(key.getPubKey())) != null;
            checkState(a == b);   // Should be in both maps or neither.
            return a;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Returns the earliest creation time of keys in this chain.
     * @return earliest creation times of keys in this chain,
     *         {@link Instant#EPOCH} if at least one time is unknown,
     *         {@link Instant#MAX} if no keys in this chain
     */
    @Override
    public Instant earliestKeyCreationTime() {
        lock.lock();
        try {
            return hashToKeys.values().stream()
                    .map(key -> key.creationTime().orElse(Instant.EPOCH))
                    .min(Instant::compareTo)
                    .orElse(Instant.MAX);
        } finally {
            lock.unlock();
        }
    }

    public List> getListeners() {
        return new ArrayList<>(listeners);
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // Serialization support
    //
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Serialize to a map of {@code Protos.Key.Builder}
     * 

Returns a {@link LinkedHashMap} which preserves iteration order. * @return A map (treat as unmodifiable) */ Map serializeToEditableProtobufs() { // Both hashToKeys and the returned map are LinkedHashMap to preserve order return hashToKeys.values().stream() .collect(Collectors.toMap(ecKey -> ecKey, // key is ECKey ecKey -> toProtoKeyBuilder(ecKey), // value is Builder (oldVal, newVal) -> newVal, // if duplicate key, overwrite oldVal with newVal LinkedHashMap::new)); // Use LinkedHashMap to preserve element order } // Create a Protos.Key.Builder from an ECKey private static Protos.Key.Builder toProtoKeyBuilder(ECKey ecKey) { Protos.Key.Builder protoKey = serializeEncryptableItem(ecKey); protoKey.setPublicKey(ByteString.copyFrom(ecKey.getPubKey())); return protoKey; } /** * Serialize to a list of keys * @return list of keys (treat as unmodifiable list, will change in future release) */ @Override public List serializeToProtobuf() { // TODO: Return unmodifiable list return serializeToEditableProtobufs().values().stream() .map(Protos.Key.Builder::build) .collect(Collectors.toList()); } /*package*/ static Protos.Key.Builder serializeEncryptableItem(EncryptableItem item) { Protos.Key.Builder proto = Protos.Key.newBuilder(); item.creationTime().ifPresent(creationTime -> proto.setCreationTimestamp(creationTime.toEpochMilli())); if (item.isEncrypted() && item.getEncryptedData() != null) { // The encrypted data can be missing for an "encrypted" key in the case of a deterministic wallet for // which the leaf keys chain to an encrypted parent and rederive their private keys on the fly. In that // case the caller in DeterministicKeyChain will take care of setting the type. EncryptedData data = item.getEncryptedData(); proto.setEncryptedData(proto.getEncryptedData().toBuilder() .setEncryptedPrivateKey(ByteString.copyFrom(data.encryptedBytes)) .setInitialisationVector(ByteString.copyFrom(data.initialisationVector))); // We don't allow mixing of encryption types at the moment. checkState(item.getEncryptionType() == Protos.Wallet.EncryptionType.ENCRYPTED_SCRYPT_AES); proto.setType(Protos.Key.Type.ENCRYPTED_SCRYPT_AES); } else { final byte[] secret = item.getSecretBytes(); // The secret might be missing in the case of a watching wallet, or a key for which the private key // is expected to be rederived on the fly from its parent. if (secret != null) proto.setSecretBytes(ByteString.copyFrom(secret)); proto.setType(Protos.Key.Type.ORIGINAL); } return proto; } /** * Returns a new BasicKeyChain that contains all basic, ORIGINAL type keys extracted from the list. Unrecognised * key types are ignored. */ public static BasicKeyChain fromProtobufUnencrypted(List keys) throws UnreadableWalletException { BasicKeyChain chain = new BasicKeyChain(); chain.deserializeFromProtobuf(keys); return chain; } /** * Returns a new BasicKeyChain that contains all basic, ORIGINAL type keys and also any encrypted keys extracted * from the list. Unrecognised key types are ignored. * @throws org.bitcoinj.wallet.UnreadableWalletException.BadPassword if the password doesn't seem to match * @throws org.bitcoinj.wallet.UnreadableWalletException if the data structures are corrupted/inconsistent */ public static BasicKeyChain fromProtobufEncrypted(List keys, KeyCrypter crypter) throws UnreadableWalletException { BasicKeyChain chain = new BasicKeyChain(Objects.requireNonNull(crypter)); chain.deserializeFromProtobuf(keys); return chain; } private void deserializeFromProtobuf(List keys) throws UnreadableWalletException { lock.lock(); try { checkState(hashToKeys.isEmpty(), () -> "tried to deserialize into a non-empty chain"); for (Protos.Key key : keys) { if (key.getType() != Protos.Key.Type.ORIGINAL && key.getType() != Protos.Key.Type.ENCRYPTED_SCRYPT_AES) continue; boolean encrypted = key.getType() == Protos.Key.Type.ENCRYPTED_SCRYPT_AES; byte[] priv = key.hasSecretBytes() ? key.getSecretBytes().toByteArray() : null; if (!key.hasPublicKey()) throw new UnreadableWalletException("Public key missing"); byte[] pub = key.getPublicKey().toByteArray(); ECKey ecKey; if (encrypted) { checkState(keyCrypter != null, () -> "this wallet is encrypted but encrypt() was not called prior to deserialization"); if (!key.hasEncryptedData()) throw new UnreadableWalletException("Encrypted private key data missing"); Protos.EncryptedData proto = key.getEncryptedData(); EncryptedData e = new EncryptedData(proto.getInitialisationVector().toByteArray(), proto.getEncryptedPrivateKey().toByteArray()); ecKey = ECKey.fromEncrypted(e, keyCrypter, pub); } else { if (priv != null) ecKey = ECKey.fromPrivateAndPrecalculatedPublic(priv, pub); else ecKey = ECKey.fromPublicOnly(pub); } ecKey.setCreationTime(Instant.ofEpochMilli(key.getCreationTimestamp())); importKeyLocked(ecKey); } } finally { lock.unlock(); } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Event listener support // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @Override public void addEventListener(KeyChainEventListener listener) { addEventListener(listener, Threading.USER_THREAD); } @Override public void addEventListener(KeyChainEventListener listener, Executor executor) { addEventListener(new ListenerRegistration<>(listener, executor)); } /* package private */ void addEventListener(ListenerRegistration listener) { listeners.add(listener); } @Override public boolean removeEventListener(KeyChainEventListener listener) { return ListenerRegistration.removeFromList(listener, listeners); } private void queueOnKeysAdded(final List keys) { checkState(lock.isHeldByCurrentThread()); for (final ListenerRegistration registration : listeners) { registration.executor.execute(() -> registration.listener.onKeysAdded(keys)); } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Encryption support // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Convenience wrapper around {@link #toEncrypted(KeyCrypter, * AesKey)} which uses the default Scrypt key derivation algorithm and * parameters, derives a key from the given password and returns the created key. */ @Override public BasicKeyChain toEncrypted(CharSequence password) { Objects.requireNonNull(password); checkArgument(password.length() > 0); KeyCrypter scrypt = new KeyCrypterScrypt(); AesKey derivedKey = scrypt.deriveKey(password); return toEncrypted(scrypt, derivedKey); } /** * Encrypt the wallet using the KeyCrypter and the AES key. A good default KeyCrypter to use is * {@link KeyCrypterScrypt}. * * @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. */ @Override public BasicKeyChain toEncrypted(KeyCrypter keyCrypter, AesKey aesKey) { lock.lock(); try { Objects.requireNonNull(keyCrypter); checkState(this.keyCrypter == null, () -> "key chain is already encrypted"); BasicKeyChain encrypted = new BasicKeyChain(keyCrypter); for (ECKey key : hashToKeys.values()) { 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)) throw new KeyCrypterException("The key " + key.toString() + " cannot be successfully decrypted after encryption so aborting wallet encryption."); encrypted.importKeyLocked(encryptedKey); } for (ListenerRegistration listener : listeners) { encrypted.addEventListener(listener); } return encrypted; } finally { lock.unlock(); } } @Override public BasicKeyChain toDecrypted(CharSequence password) { Objects.requireNonNull(keyCrypter, "Wallet is already decrypted"); AesKey aesKey = keyCrypter.deriveKey(password); return toDecrypted(aesKey); } @Override public BasicKeyChain toDecrypted(AesKey aesKey) { lock.lock(); try { checkState(keyCrypter != null, () -> "wallet is already decrypted"); // Do an up-front check. if (numKeys() > 0 && !checkAESKey(aesKey)) throw new KeyCrypterException("Password/key was incorrect."); BasicKeyChain decrypted = new BasicKeyChain(); for (ECKey key : hashToKeys.values()) { decrypted.importKeyLocked(key.decrypt(aesKey)); } for (ListenerRegistration listener : listeners) { decrypted.addEventListener(listener); } return decrypted; } finally { lock.unlock(); } } /** * Returns whether the given password is correct for this key chain. * @throws IllegalStateException if the chain is not encrypted at all. */ @Override public boolean checkPassword(CharSequence password) { Objects.requireNonNull(password); checkState(keyCrypter != null, () -> "key chain not encrypted"); return checkAESKey(keyCrypter.deriveKey(password)); } /** * Check whether the AES key can decrypt the first encrypted key in the wallet. * * @return true if AES key supplied can decrypt the first encrypted private key in the wallet, false otherwise. */ @Override public boolean checkAESKey(AesKey aesKey) { lock.lock(); try { // If no keys then cannot decrypt. if (hashToKeys.isEmpty()) return false; checkState(keyCrypter != null, () -> "key chain is not encrypted"); // Find the first encrypted key in the wallet. ECKey first = null; for (ECKey key : hashToKeys.values()) { if (key.isEncrypted()) { first = key; break; } } checkState(first != null, () -> "no encrypted keys in the wallet"); try { ECKey rebornKey = first.decrypt(aesKey); return Arrays.equals(first.getPubKey(), rebornKey.getPubKey()); } catch (KeyCrypterException e) { // The AES key supplied is incorrect. return false; } } finally { lock.unlock(); } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Bloom filtering support // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @Override public BloomFilter getFilter(int size, double falsePositiveRate, int tweak) { lock.lock(); try { BloomFilter filter = new BloomFilter(size, falsePositiveRate, tweak); for (ECKey key : hashToKeys.values()) filter.insert(key); return filter; } finally { lock.unlock(); } } @Override public int numBloomFilterEntries() { return numKeys() * 2; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Key rotation support // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** Returns the first ECKey created after the given time, or empty if there is none. */ public Optional findOldestKeyAfter(Instant time) { lock.lock(); try { ECKey oldest = null; for (ECKey key : hashToKeys.values()) { Instant keyTime = key.creationTime().orElse(Instant.EPOCH); if (keyTime.isAfter(time)) { if (oldest == null || oldest.creationTime().orElse(Instant.EPOCH).isAfter(keyTime)) oldest = key; } } return Optional.ofNullable(oldest); } finally { lock.unlock(); } } /** @deprecated use {@link #findOldestKeyAfter(Instant)} */ @Nullable @Deprecated public ECKey findOldestKeyAfter(long timeSecs) { return findOldestKeyAfter(Instant.ofEpochSecond(timeSecs)).orElse(null); } /** Returns a list of all ECKeys created after the given time. */ public List findKeysBefore(Instant time) { lock.lock(); try { List results = new LinkedList<>(); for (ECKey key : hashToKeys.values()) { Instant keyTime = key.creationTime().orElse(Instant.EPOCH); if (keyTime.isBefore(time)) { results.add(key); } } return results; } finally { lock.unlock(); } } /** @deprecated use {@link #findKeysBefore(Instant)} */ @Deprecated public List findKeysBefore(long timeSecs) { return findKeysBefore(Instant.ofEpochSecond(timeSecs)); } public String toString(boolean includePrivateKeys, @Nullable AesKey aesKey, NetworkParameters params) { final StringBuilder builder = new StringBuilder(); List keys = getKeys(); Collections.sort(keys, ECKey.AGE_COMPARATOR); for (ECKey key : keys) key.formatKeyWithAddress(includePrivateKeys, aesKey, builder, params.network(), null, "imported"); return builder.toString(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy