Java tutorial
/* * Copyright 2013 bits of proof zrt. * * 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 com.bitsofproof.supernode.api; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SecureRandom; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.Mac; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import org.bouncycastle.asn1.sec.SECNamedCurves; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.generators.SCrypt; import org.bouncycastle.math.ec.ECPoint; import org.bouncycastle.util.Arrays; import com.bitsofproof.supernode.common.ByteUtils; import com.bitsofproof.supernode.common.ECKeyPair; import com.bitsofproof.supernode.common.ECPublicKey; import com.bitsofproof.supernode.common.Key; import com.bitsofproof.supernode.common.ValidationException; /** * Key Generator following BIP32 https://en.bitcoin.it/wiki/BIP_0032 */ public class ExtendedKey { private static final SecureRandom rnd = new SecureRandom(); private static final X9ECParameters curve = SECNamedCurves.getByName("secp256k1"); private final Key master; private final byte[] chainCode; private final int depth; private final int parent; private final int sequence; private static final byte[] BITCOIN_SEED = "Bitcoin seed".getBytes(); public static ExtendedKey createFromPassphrase(String passphrase, byte[] encryptedSeed) throws ValidationException { try { byte[] key = SCrypt.generate(passphrase.getBytes("UTF-8"), BITCOIN_SEED, 16384, 8, 8, 32); Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding", "BC"); SecretKeySpec keyspec = new SecretKeySpec(key, "AES"); cipher.init(Cipher.DECRYPT_MODE, keyspec); return create(cipher.doFinal(encryptedSeed)); } catch (UnsupportedEncodingException e) { throw new ValidationException(e); } catch (IllegalBlockSizeException e) { throw new ValidationException(e); } catch (BadPaddingException e) { throw new ValidationException(e); } catch (InvalidKeyException e) { throw new ValidationException(e); } catch (NoSuchAlgorithmException e) { throw new ValidationException(e); } catch (NoSuchProviderException e) { throw new ValidationException(e); } catch (NoSuchPaddingException e) { throw new ValidationException(e); } } public static ExtendedKey create(byte[] seed) throws ValidationException { try { Mac mac = Mac.getInstance("HmacSHA512", "BC"); 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) { throw new ValidationException("This is rather unlikely, but it did just happen"); } ECKeyPair keyPair = new ECKeyPair(l, true); return new ExtendedKey(keyPair, r, 0, 0, 0); } catch (NoSuchAlgorithmException e) { throw new ValidationException(e); } catch (NoSuchProviderException e) { throw new ValidationException(e); } catch (InvalidKeyException e) { throw new ValidationException(e); } } public static ExtendedKey createNew() { Key key = ECKeyPair.createNew(true); byte[] chainCode = new byte[32]; rnd.nextBytes(chainCode); return new ExtendedKey(key, chainCode, 0, 0, 0); } public ExtendedKey(Key key, byte[] chainCode, int depth, int parent, int sequence) { this.master = key; this.chainCode = chainCode; this.parent = parent; this.depth = depth; this.sequence = sequence; } public Key getMaster() { return master; } public byte[] getChainCode() { return Arrays.clone(chainCode); } public int getDepth() { return depth; } public int getParent() { return parent; } public int getSequence() { return sequence; } public int getFingerPrint() { int fingerprint = 0; byte[] address = master.getAddress(); for (int i = 0; i < 4; ++i) { fingerprint <<= 8; fingerprint |= address[i] & 0xff; } return fingerprint; } public Key getKey(int sequence) throws ValidationException { return generateKey(sequence).getMaster(); } public ExtendedKey getChild(int sequence) throws ValidationException { ExtendedKey sub = generateKey(sequence); return new ExtendedKey(sub.getMaster(), sub.getChainCode(), sub.getDepth() + 1, getFingerPrint(), sequence); } public ExtendedKey getReadOnly() { return new ExtendedKey(new ECPublicKey(master.getPublic(), true), chainCode, depth, parent, sequence); } public boolean isReadOnly() { return master.getPrivate() == null; } private ExtendedKey generateKey(int sequence) throws ValidationException { try { if ((sequence & 0x80000000) != 0 && master.getPrivate() == null) { throw new ValidationException("need private key for private generation"); } Mac mac = Mac.getInstance("HmacSHA512", "BC"); SecretKey key = new SecretKeySpec(chainCode, "HmacSHA512"); mac.init(key); byte[] extended; byte[] pub = master.getPublic(); if ((sequence & 0x80000000) == 0) { extended = new byte[pub.length + 4]; System.arraycopy(pub, 0, extended, 0, pub.length); extended[pub.length] = (byte) ((sequence >>> 24) & 0xff); extended[pub.length + 1] = (byte) ((sequence >>> 16) & 0xff); extended[pub.length + 2] = (byte) ((sequence >>> 8) & 0xff); extended[pub.length + 3] = (byte) (sequence & 0xff); } else { byte[] priv = master.getPrivate(); extended = new byte[priv.length + 5]; System.arraycopy(priv, 0, extended, 1, priv.length); extended[priv.length + 1] = (byte) ((sequence >>> 24) & 0xff); extended[priv.length + 2] = (byte) ((sequence >>> 16) & 0xff); extended[priv.length + 3] = (byte) ((sequence >>> 8) & 0xff); extended[priv.length + 4] = (byte) (sequence & 0xff); } byte[] lr = mac.doFinal(extended); 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) { throw new ValidationException("This is rather unlikely, but it did just happen"); } if (master.getPrivate() != null) { BigInteger k = m.add(new BigInteger(1, master.getPrivate())).mod(curve.getN()); if (k.equals(BigInteger.ZERO)) { throw new ValidationException("This is rather unlikely, but it did just happen"); } return new ExtendedKey(new ECKeyPair(k, true), r, depth, parent, sequence); } else { ECPoint q = curve.getG().multiply(m).add(curve.getCurve().decodePoint(pub)); if (q.isInfinity()) { throw new ValidationException("This is rather unlikely, but it did just happen"); } pub = new ECPoint.Fp(curve.getCurve(), q.getX(), q.getY(), true).getEncoded(); return new ExtendedKey(new ECPublicKey(pub, true), r, depth, parent, sequence); } } catch (NoSuchAlgorithmException e) { throw new ValidationException(e); } catch (NoSuchProviderException e) { throw new ValidationException(e); } catch (InvalidKeyException e) { throw new ValidationException(e); } } 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 }; public String serialize(boolean production) { ByteArrayOutputStream out = new ByteArrayOutputStream(); try { if (master.getPrivate() != null) { if (production) { out.write(xprv); } else { out.write(tprv); } } else { if (production) { out.write(xpub); } else { out.write(tpub); } } out.write(depth & 0xff); out.write((parent >>> 24) & 0xff); out.write((parent >>> 16) & 0xff); out.write((parent >>> 8) & 0xff); out.write(parent & 0xff); out.write((sequence >>> 24) & 0xff); out.write((sequence >>> 16) & 0xff); out.write((sequence >>> 8) & 0xff); out.write(sequence & 0xff); out.write(chainCode); if (master.getPrivate() != null) { out.write(0x00); out.write(master.getPrivate()); } else { out.write(master.getPublic()); } } catch (IOException e) { } return ByteUtils.toBase58WithChecksum(out.toByteArray()); } public static ExtendedKey parse(String serialized) throws ValidationException { byte[] data = ByteUtils.fromBase58WithChecksum(serialized); if (data.length != 78) { throw new ValidationException("invalid extended key"); } 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 = data[5] & 0xff; parent <<= 8; parent |= data[6] & 0xff; parent <<= 8; parent |= data[7] & 0xff; parent <<= 8; parent |= data[8] & 0xff; int sequence = data[9] & 0xff; sequence <<= 8; sequence |= data[10] & 0xff; sequence <<= 8; sequence |= data[11] & 0xff; sequence <<= 8; sequence |= data[12] & 0xff; byte[] chainCode = Arrays.copyOfRange(data, 13, 13 + 32); byte[] pubOrPriv = Arrays.copyOfRange(data, 13 + 32, data.length); Key key; if (hasPrivate) { key = new ECKeyPair(new BigInteger(1, pubOrPriv), true); } else { key = new ECPublicKey(pubOrPriv, true); } return new ExtendedKey(key, chainCode, depth, parent, sequence); } }