us.eharning.atomun.keygen.internal.spi.bip0032.BouncyCastleBIP0032NodeProcessor.java Source code

Java tutorial

Introduction

Here is the source code for us.eharning.atomun.keygen.internal.spi.bip0032.BouncyCastleBIP0032NodeProcessor.java

Source

/*
 * Copyright 2015 Thomas Harning Jr. <harningt@gmail.com>
 *
 * 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 us.eharning.atomun.keygen.internal.spi.bip0032;

import com.google.common.base.Charsets;
import com.google.common.base.Objects;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.bouncycastle.asn1.sec.SECNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.Arrays;
import us.eharning.atomun.core.ValidationException;
import us.eharning.atomun.core.ec.ECKey;
import us.eharning.atomun.core.ec.ECKeyFactory;
import us.eharning.atomun.core.encoding.Base58;
import us.eharning.atomun.keygen.path.BIP0032Path;

import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.annotation.Nonnull;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

/**
 * BIP0032 node processing implementation based on BouncyCastle.
 */
final class BouncyCastleBIP0032NodeProcessor implements BIP0032NodeProcessor {
    /**
     * Cache for already-processed node-encodings to avoid unnecessary re-derivation.
     */
    public static final Cache<NodeSequence, BIP0032Node> NODE_CACHE = CacheBuilder.newBuilder().maximumSize(16)
            .recordStats().build();
    private static final SecureRandom rnd = new SecureRandom();
    private static final X9ECParameters curve = SECNamedCurves.getByName("secp256k1");
    private static final byte[] BITCOIN_SEED = "Bitcoin seed".getBytes(Charsets.US_ASCII);
    private static final byte[] xprv = new byte[] { 0x04, (byte) 0x88, (byte) 0xAD, (byte) 0xE4 };
    private static final byte[] xpub = new byte[] { 0x04, (byte) 0x88, (byte) 0xB2, (byte) 0x1E };
    private static final byte[] tprv = new byte[] { 0x04, (byte) 0x35, (byte) 0x83, (byte) 0x94 };
    private static final byte[] tpub = new byte[] { 0x04, (byte) 0x35, (byte) 0x87, (byte) 0xCF };

    /**
     * Copy the given data bytes into the output array at a given offset.
     *
     * @param out
     *         array to write into.
     * @param index
     *         offset into the array to write at.
     * @param data
     *         array to write from.
     */
    private static void putBytes(byte[] out, int index, byte[] data) {
        System.arraycopy(data, 0, out, index, data.length);
    }

    /**
     * Store an integer as 4 bytes in a byte array.
     *
     * @param out
     *         array to write into.
     * @param index
     *         offset into the array to write at.
     * @param value
     *         value to write into the array.
     */
    private static void putInt32(@Nonnull byte[] out, int index, int value) {
        out[index] = (byte) (value >> 24);
        out[index + 1] = (byte) (value >> 16);
        out[index + 2] = (byte) (value >> 8);
        out[index + 3] = (byte) value;
    }

    /**
     * Convert 4 bytes of a byte array to an integer.
     *
     * @param in
     *         byte array to obtain the integer from.
     * @param index
     *         offset into the array to start at.
     *
     * @return 32-bit integer from the input byte array.
     */
    private static int getInt32(@Nonnull byte[] in, int index) {
        return (in[index] & 0xff) << 24 | (in[index + 1] & 0xff) << 16 | (in[index + 2] & 0xff) << 8
                | (in[index + 3] & 0xff);
    }

    /**
     * Convert the node into a Base58+checksum encoded string.
     *
     * @param node
     *         instance to encode.
     *
     * @return Base58+checksum encoded string.
     */
    @Override
    public String exportNode(BIP0032Node node) {
        /* Assume production - TODO: allow custom address bases */
        final boolean production = true;
        final ECKey master = node.getMaster();
        byte[] out = new byte[78];
        if (master.hasPrivate()) {
            if (production) {
                putBytes(out, 0, xprv);
            } else {
                putBytes(out, 0, tprv);
            }
        } else {
            if (production) {
                putBytes(out, 0, xpub);
            } else {
                putBytes(out, 0, tpub);
            }
        }
        out[4] = (byte) (node.getDepth() & 0xff);
        putInt32(out, 5, node.getParent());
        putInt32(out, 9, node.getSequence());
        putBytes(out, 13, node.getChainCode());
        if (master.hasPrivate()) {
            out[45] = 0x00;
            byte[] privateKey = master.exportPrivate();
            assert (null != privateKey);
            putBytes(out, 46, privateKey);
        } else {
            putBytes(out, 45, master.exportPublic());
        }
        return Base58.encodeWithChecksum(out);
    }

    /**
     * Convert the Base58+checksum encoded string into a BIP0032 node.
     *
     * @param serialized
     *         encoded string to decode.
     *
     * @return BIP0032 node represented by the serialized string.
     *
     * @throws ValidationException
     *         if the data is not a valid encoded BIP0032 node.
     */
    @Override
    public BIP0032Node importNode(String serialized) throws ValidationException {
        byte[] data = Base58.decodeWithChecksum(serialized);
        if (data.length != 78) {
            throw new ValidationException("Invalid extended key value");
        }
        byte[] type = Arrays.copyOf(data, 4);
        boolean hasPrivate;
        if (Arrays.areEqual(type, xprv) || Arrays.areEqual(type, tprv)) {
            hasPrivate = true;
        } else if (Arrays.areEqual(type, xpub) || Arrays.areEqual(type, tpub)) {
            hasPrivate = false;
        } else {
            throw new ValidationException("Invalid magic number for an extended key");
        }
        int depth = data[4] & 0xff;
        int parent = getInt32(data, 5);
        int sequence = getInt32(data, 9);
        byte[] chainCode = Arrays.copyOfRange(data, 13, 13 + 32);
        byte[] pubOrPriv = Arrays.copyOfRange(data, 13 + 32, data.length);
        ECKey key;
        if (hasPrivate) {
            key = ECKeyFactory.getInstance().fromSecretExponent(new BigInteger(1, pubOrPriv), true);
        } else {
            key = ECKeyFactory.getInstance().fromEncodedPublicKey(pubOrPriv, true);
        }
        return new BIP0032Node(key, chainCode, depth, parent, sequence);
    }

    /**
     * Generates a BIP0032 node from a seed value that is passed through a basic HMAC process.
     *
     * @param seed
     *         value for which the node should be generated.
     *
     * @return BIP0032 node deterministically based on the seed input.
     *
     * @throws ValidationException
     *         if either cryptography fails (unlikely)
     *         or the seed results in an invalid EC key (unlikely).
     */
    /* BITCOIN_SEED is not a password but instead a shared key to mask the seed input */
    @SuppressFBWarnings("HARD_CODE_PASSWORD")
    @SuppressWarnings("checkstyle:localvariablename")
    @Override
    public BIP0032Node generateNodeFromSeed(byte[] seed) throws ValidationException {
        try {
            Mac mac = Mac.getInstance("HmacSHA512");
            SecretKey seedkey = new SecretKeySpec(BITCOIN_SEED, "HmacSHA512");
            mac.init(seedkey);
            byte[] lr = mac.doFinal(seed);
            byte[] l = Arrays.copyOfRange(lr, 0, 32);
            byte[] r = Arrays.copyOfRange(lr, 32, 64);
            BigInteger m = new BigInteger(1, l);
            if (m.compareTo(curve.getN()) >= 0 || m.compareTo(BigInteger.ZERO) == 0) {
                throw new ValidationException("Invalid chain value generated");
            }
            ECKey keyPair = ECKeyFactory.getInstance().fromSecretExponent(m, true);
            return new BIP0032Node(keyPair, r, 0, 0, 0);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            throw new ValidationException(e);
        }
    }

    /**
     * Generates a random BIP0032 node.
     *
     * @return BIP0032 node randomly generated.
     */
    @Override
    public BIP0032Node generateNode() {
        ECKey key = ECKeyFactory.getInstance().generateRandom(true);
        byte[] chainCode = new byte[32];
        rnd.nextBytes(chainCode);
        return new BIP0032Node(key, chainCode, 0, 0, 0);
    }

    /**
     * Utility method to perform 'I' value derivation.
     *
     * @param node
     *         BIP0032 base node.
     * @param sequence
     *         value to use for derivation.
     *
     * @return derived BIP0032 node.
     *
     * @throws NoSuchAlgorithmException
     *         if somehow HmacSHA512 cannot be found (unlikely).
     * @throws InvalidKeyException
     *         if the chain code is somehow not a value HmacSHA512 key (unlikely).
     */
    @Nonnull
    private byte[] deriveI(@Nonnull BIP0032Node node, int sequence)
            throws NoSuchAlgorithmException, InvalidKeyException {
        Mac mac = Mac.getInstance("HmacSHA512");
        SecretKey key = new SecretKeySpec(node.getChainCode(), "HmacSHA512");
        mac.init(key);
        byte[] extended;
        if ((sequence & 0x80000000) == 0) {
            byte[] pub = node.getMaster().exportPublic();
            extended = java.util.Arrays.copyOf(pub, pub.length + 4);
            putInt32(extended, pub.length, sequence);
        } else {
            byte[] priv = node.getMaster().exportPrivate();
            assert (null != priv);
            extended = new byte[priv.length + 5];
            /* Offset of 1 to account for extra zero at front */
            System.arraycopy(priv, 0, extended, 1, priv.length);
            putInt32(extended, priv.length + 1, sequence);
        }
        return mac.doFinal(extended);
    }

    /**
     * Derives a BIP0032 node given the input path.
     *
     * @param node
     *         base node to derive from.
     * @param path
     *         set of sequence values to use for derivation.
     *
     * @return BIP0032 node derived using the necessary algorithms per BIP0032 specification.
     *
     * @throws ValidationException
     *         if it is impossible to generate a key using the path,
     *         it is impossible to derive a key due to missing private bits,
     *         or the resultant key is an invalid EC key (unlikely).
     */
    @Override
    public BIP0032Node deriveNode(BIP0032Node node, BIP0032Path path) throws ValidationException {
        BIP0032Node current = node;
        /* NOTE: since relative paths aren't possible yet with path object, must derive from root */
        if (current.getDepth() != 0) {
            throw new ValidationException("Wrong depth for root-based path");
        }
        for (Integer sequence : path) {
            current = deriveNode(current, sequence);
        }
        return current;
    }

    /**
     * Derives a BIP0032 node given the singular sequence value.
     *
     * @param node
     *         base node to derive from.
     * @param sequence
     *         value to use for derivation.
     *
     * @return BIP0032 node derived using the necessary algorithms per BIP0032 specification.
     *
     * @throws ValidationException
     *         it is impossible to derive a key due to missing private bits,
     *         or the resultant key is an invalid EC key (unlikely).
     */
    @SuppressWarnings("checkstyle:localvariablename")
    @Override
    public BIP0032Node deriveNode(BIP0032Node node, int sequence) throws ValidationException {
        NodeSequence nodeSequence = new NodeSequence(node, sequence);
        BIP0032Node cached = NODE_CACHE.getIfPresent(nodeSequence);
        if (null != cached) {
            return cached;
        }
        final ECKey master = node.getMaster();
        try {
            if ((sequence & 0x80000000) != 0 && master.exportPrivate() == null) {
                throw new ValidationException("Need private key for private generation");
            }
            byte[] lr = deriveI(node, sequence);

            byte[] l = java.util.Arrays.copyOfRange(lr, 0, 32);
            byte[] r = java.util.Arrays.copyOfRange(lr, 32, 64);
            BigInteger m = new BigInteger(1, l);
            if (m.compareTo(curve.getN()) >= 0 || m.compareTo(BigInteger.ZERO) == 0) {
                throw new ValidationException("Invalid chain value generated");
            }
            if (master.hasPrivate()) {
                byte[] priv = master.exportPrivate();
                assert (null != priv);
                BigInteger k = m.add(new BigInteger(1, priv)).mod(curve.getN());
                if (k.compareTo(BigInteger.ZERO) == 0) {
                    throw new ValidationException("Invalid private node generated");
                }
                cached = new BIP0032Node(ECKeyFactory.getInstance().fromSecretExponent(k, true), r,
                        node.getDepth() + 1, node.getFingerPrint(), sequence);
                NODE_CACHE.put(nodeSequence, cached);
                return cached;
            } else {
                byte[] pub = master.exportPublic();
                ECPoint q = curve.getG().multiply(m).add(curve.getCurve().decodePoint(pub));
                if (q.isInfinity()) {
                    throw new ValidationException("Invalid public node generated");
                }
                pub = q.getEncoded(true);
                cached = new BIP0032Node(ECKeyFactory.getInstance().fromEncodedPublicKey(pub, true), r,
                        node.getDepth() + 1, node.getFingerPrint(), sequence);
                NODE_CACHE.put(nodeSequence, cached);
                return cached;
            }
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            throw new ValidationException(e);
        }
    }

    /**
     * Obtain the BIP0032 node without its private bits (if present).
     *
     * @param node
     *         instance to obtain a version of without private bits.
     *
     * @return BIP0032 node instance without private bits.
     */
    public BIP0032Node getPublic(BIP0032Node node) {
        if (!node.hasPrivate()) {
            return node;
        }
        final ECKey master = ECKeyFactory.getInstance().fromEncodedPublicKey(node.getMaster().exportPublic(), true);
        return new BIP0032Node(master, node.getChainCode(), node.getDepth(), node.getParent(), node.getSequence());
    }

    /**
     * Utility class wrapping a BIP0032Node+sequence pair for cache identity entry.
     */
    private static final class NodeSequence {
        private final BIP0032Node node;
        private final int sequence;

        /**
         * Construct a new pair.
         *
         * @param node
         *         BIP0032 base node.
         * @param sequence
         *         index into BIP0032 base node.
         */
        public NodeSequence(@Nonnull BIP0032Node node, int sequence) {
            this.node = node;
            this.sequence = sequence;
        }

        /**
         * Returns whether this pair is equivalent to another.
         *
         * @param obj
         *         instance to compare against.
         *
         * @return true if the objects are identical, false otherwise.
         */
        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null || getClass() != obj.getClass()) {
                return false;
            }
            NodeSequence that = (NodeSequence) obj;
            return Objects.equal(sequence, that.sequence) && Objects.equal(node, that.node);
        }

        /**
         * Returns a hash code value for the object.
         *
         * @return a hash code value for this object.
         */
        @Override
        public int hashCode() {
            return Objects.hashCode(node, sequence);
        }
    }
}