package io.emax.cosigner.bitcoin.common; import io.emax.cosigner.bitcoin.BitcoinResource; import io.emax.cosigner.bitcoin.bitcoindrpc.BlockChainName; import io.emax.cosigner.bitcoin.bitcoindrpc.NetworkBytes; import io.emax.cosigner.common.Base58; import io.emax.cosigner.common.ByteUtilities; import io.emax.cosigner.common.DeterministicRng; import io.emax.cosigner.common.crypto.Secp256k1; import org.bouncycastle.crypto.digests.RIPEMD160Digest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; import java.util.Locale; public class BitcoinTools { private static final Logger LOGGER = LoggerFactory.getLogger(BitcoinTools.class); public static final String NOKEY = "NOKEY"; private static final String SHA256 = "SHA-256"; /** * Generate a deterministic set of private keys based on a secret key. * * @param userKeyPart Expect these to be hex strings without the leading 0x identifier. When * combined with serverKeyPart, it provides the seed for the private keys. * @param serverKeyPart Expect these to be hex strings without the leading 0x identifier. When * combined with userKeyPart, it provides the seed for the private keys. * @param rounds Number of keys to skip when generating the private key. * @return The private key that this data generates. */ public static String getDeterministicPrivateKey(String userKeyPart, String serverKeyPart, int rounds) { if (userKeyPart == null) { return NOKEY; } try { byte[] userKey = new BigInteger(userKeyPart, 16).toByteArray(); byte[] serverKey = new BigInteger(serverKeyPart, 16).toByteArray(); SecureRandom secureRandom = DeterministicRng.getSecureRandom(userKey, serverKey); // Generate the key, skipping as many as desired. byte[] privateKeyAttempt = new byte[32]; for (int i = 0; i < Math.max(rounds, 1); i++) { secureRandom.nextBytes(privateKeyAttempt); BigInteger privateKeyCheck = new BigInteger(1, privateKeyAttempt); while (privateKeyCheck.compareTo(BigInteger.ZERO) == 0 || privateKeyCheck.compareTo(Secp256k1.MAXPRIVATEKEY) == 1) { secureRandom.nextBytes(privateKeyAttempt); privateKeyCheck = new BigInteger(1, privateKeyAttempt); } } return encodePrivateKey(ByteUtilities.toHexString(privateKeyAttempt)); } catch (RuntimeException e) { LOGGER.debug(null, e); return NOKEY; } } /** * Encodes a raw public key in a bitcoind compatible format. */ public static String encodePrivateKey(String privateKeyString) { String networkBytes; try { networkBytes = BitcoinResource.getResource().getBitcoindRpc().getblockchaininfo().getChain() == BlockChainName.main ? NetworkBytes.PRIVATEKEY.toString() : NetworkBytes.PRIVATEKEY_TEST.toString(); } catch (Exception e) { LOGGER.debug("No network connection, assuming regular network", e); networkBytes = NetworkBytes.PRIVATEKEY.toString(); } // Encode in format bitcoind is expecting byte[] privateKeyAttempt = ByteUtilities.toByteArray(privateKeyString); byte[] privateKey = ByteUtilities.toByteArray(networkBytes); byte[] privateKey2 = new byte[privateKey.length + privateKeyAttempt.length]; System.arraycopy(privateKey, 0, privateKey2, 0, privateKey.length); System .arraycopy(privateKeyAttempt, 0, privateKey2, privateKey.length, privateKeyAttempt.length); privateKey = new byte[privateKey2.length]; System.arraycopy(privateKey2, 0, privateKey, 0, privateKey2.length); try { MessageDigest md = MessageDigest.getInstance(SHA256); md.update(privateKey); byte[] checksumHash = Arrays.copyOfRange(md.digest(md.digest()), 0, 4); privateKey2 = new byte[privateKey.length + checksumHash.length]; System.arraycopy(privateKey, 0, privateKey2, 0, privateKey.length); System.arraycopy(checksumHash, 0, privateKey2, privateKey.length, checksumHash.length); privateKey = new byte[privateKey2.length]; System.arraycopy(privateKey2, 0, privateKey, 0, privateKey2.length); return Base58.encode(privateKey); } catch (NoSuchAlgorithmException e) { LOGGER.error(null, e); return NOKEY; } } /** * Encodes the userKey secret so that it can be referenced and stored in bitcoind's wallet without * revealing what the original value is. * * @param key User key secret value. * @return Encoded/hashed version of the key. */ public static String encodeUserKey(String key) { try { MessageDigest md = MessageDigest.getInstance(SHA256); md.update(key.getBytes("UTF-8")); return new BigInteger(md.digest()).toString(16); } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { LOGGER.error(null, e); return null; } } /** * Convert a key into its corresponding public address. * * @param key Key to convert * @param isPrivateKey Is this private or public * @return Public bitcoin address. */ public static String getPublicAddress(String key, boolean isPrivateKey) { try { byte[] publicKeyBytes; if (isPrivateKey) { publicKeyBytes = getPublicKeyBytes(key); } else { publicKeyBytes = ByteUtilities.toByteArray(key); } MessageDigest md = MessageDigest.getInstance(SHA256); md.reset(); md.update(publicKeyBytes); byte[] publicShaKeyBytes = md.digest(); RIPEMD160Digest ripemd = new RIPEMD160Digest(); byte[] publicRipemdKeyBytes = new byte[20]; ripemd.update(publicShaKeyBytes, 0, publicShaKeyBytes.length); ripemd.doFinal(publicRipemdKeyBytes, 0); // Add network bytes String networkBytes = BitcoinResource.getResource().getBitcoindRpc().getblockchaininfo().getChain() == BlockChainName.main ? NetworkBytes.P2PKH.toString() : NetworkBytes.P2PKH_TEST.toString(); byte[] networkPublicKeyBytes = ByteUtilities.toByteArray(networkBytes); byte[] networkPublicKeyBytes2 = new byte[networkPublicKeyBytes.length + publicRipemdKeyBytes.length]; System.arraycopy(networkPublicKeyBytes, 0, networkPublicKeyBytes2, 0, networkPublicKeyBytes.length); System .arraycopy(publicRipemdKeyBytes, 0, networkPublicKeyBytes2, networkPublicKeyBytes.length, publicRipemdKeyBytes.length); networkPublicKeyBytes = new byte[networkPublicKeyBytes2.length]; System.arraycopy(networkPublicKeyBytes2, 0, networkPublicKeyBytes, 0, networkPublicKeyBytes2.length); md = MessageDigest.getInstance(SHA256); md.reset(); md.update(networkPublicKeyBytes); byte[] publicKeyChecksum = Arrays.copyOfRange(md.digest(md.digest()), 0, 4); byte[] decodedPublicKey = new byte[networkPublicKeyBytes.length + publicKeyChecksum.length]; System.arraycopy(networkPublicKeyBytes, 0, decodedPublicKey, 0, networkPublicKeyBytes.length); System.arraycopy(publicKeyChecksum, 0, decodedPublicKey, networkPublicKeyBytes.length, publicKeyChecksum.length); return Base58.encode(decodedPublicKey); } catch (Exception e) { LOGGER.error("Unable to get network information when creating address", e); return null; } } /** * Decodes a bitcoin address and returns the RIPEMD-160 that it contains. * * @param address Bitcoin address * @return RIPEMD-160 hash of the public key. */ public static String decodeAddress(String address) { try { byte[] decodedNetworkAddress = Base58.decode(address); byte[] networkBytes = ByteUtilities.readBytes(decodedNetworkAddress, 0, 1); byte[] addressBytes = ByteUtilities.readBytes(decodedNetworkAddress, 1, decodedNetworkAddress.length - 5); String checksumString = ByteUtilities.toHexString(networkBytes) + ByteUtilities.toHexString(addressBytes); byte[] checksumData = ByteUtilities.toByteArray(checksumString); MessageDigest md = MessageDigest.getInstance(SHA256); md.reset(); byte[] calculatedCheckum = Arrays.copyOfRange(md.digest(md.digest(checksumData)), 0, 4); LOGGER.debug("Address: " + address); LOGGER.debug("DecodedAddress: " + ByteUtilities.toHexString(decodedNetworkAddress)); LOGGER.debug("NetworkBytes: " + ByteUtilities.toHexString(networkBytes)); LOGGER.debug("AddressBytes: " + ByteUtilities.toHexString(addressBytes)); byte[] checksumBytes = ByteUtilities.readBytes(decodedNetworkAddress, decodedNetworkAddress.length - 4, 4); LOGGER.debug("ChecksumBytes: " + ByteUtilities.toHexString(checksumBytes)); LOGGER.debug("CalculatedChecksum: " + ByteUtilities.toHexString(calculatedCheckum)); if (!ByteUtilities.toHexString(calculatedCheckum) .equalsIgnoreCase(ByteUtilities.toHexString(checksumBytes))) { LOGGER.debug("Badchecksum on: " + ByteUtilities.toHexString(addressBytes)); return ""; } return ByteUtilities.toHexString(addressBytes); } catch (Exception e) { LOGGER.error(null, e); return ""; } } /** * Converts a RIPEMD-160 address to a base58 encoded one with checksums. * * @param addressBytes RIPEMD-160 address * @param networkBytes Network bytes that identify which network this address belongs to. * @return Address that bitcoind can import. */ public static String encodeAddress(String addressBytes, String networkBytes) { try { String encodedBytes = networkBytes + addressBytes; byte[] data = ByteUtilities.toByteArray(encodedBytes); MessageDigest md = MessageDigest.getInstance(SHA256); md.reset(); md.update(data); byte[] publicKeyChecksum = Arrays.copyOfRange(md.digest(md.digest()), 0, 4); encodedBytes = encodedBytes + ByteUtilities.toHexString(publicKeyChecksum); encodedBytes = encodedBytes.toLowerCase(Locale.US); encodedBytes = Base58.encode(ByteUtilities.toByteArray(encodedBytes)); return encodedBytes; } catch (Exception e) { LOGGER.error(null, e); return null; } } /** * Decodes an address and checks if it's a P2SH. * * @param address Bitcoin address * @return True if it's a P2SH address, false otherwise. */ public static boolean isMultiSigAddress(String address) { try { // If the address isn't valid. if (decodeAddress(address).isEmpty()) { return false; } byte[] decodedNetworkAddress = Base58.decode(address); byte[] networkBytes = ByteUtilities.readBytes(decodedNetworkAddress, 0, 1); String networkString = ByteUtilities.toHexString(networkBytes); return networkString.equalsIgnoreCase(NetworkBytes.P2SH.toString()) || networkString .equalsIgnoreCase(NetworkBytes.P2SH_TEST.toString()); } catch (Exception e) { LOGGER.debug(null, e); return false; } } public static String getPublicKey(String privateKey) { return ByteUtilities.toHexString(getPublicKeyBytes(privateKey)); } /** * Converts a bitcoin-encoded private key to its corresponding public key. * * @param privateKey Bitcoin-encoded private key. * @return ECDSA public key. */ public static byte[] getPublicKeyBytes(String privateKey) { try { byte[] decodedPrivateKey = Base58.decode(privateKey); byte[] networkPrivateKeyBytes = new byte[decodedPrivateKey.length - 4]; byte[] privateKeyChecksum = new byte[4]; System .arraycopy(decodedPrivateKey, 0, networkPrivateKeyBytes, 0, decodedPrivateKey.length - 4); System.arraycopy(decodedPrivateKey, decodedPrivateKey.length - 4, privateKeyChecksum, 0, 4); // Is it valid? MessageDigest md = MessageDigest.getInstance(SHA256); md.update(networkPrivateKeyBytes); byte[] checksumCheck = Arrays.copyOfRange(md.digest(md.digest()), 0, 4); for (int i = 0; i < 4; i++) { if (privateKeyChecksum[i] != checksumCheck[i]) { return new byte[0]; } } // Strip leading network byte and get the public key byte[] privateKeyBytes = Arrays.copyOfRange(networkPrivateKeyBytes, 1, networkPrivateKeyBytes.length); return Secp256k1.getPublicKey(privateKeyBytes); } catch (Exception e) { LOGGER.error(null, e); return new byte[0]; } } }