/* ******************************************************************************* * BTChip Bitcoin Hardware Wallet Java API * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn * * 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.btchip; import com.btchip.comm.BTChipTransport; import com.btchip.utils.BIP32Utils; import com.btchip.utils.BufferUtils; import com.btchip.utils.CoinFormatUtils; import com.btchip.utils.Dump; import com.btchip.utils.VarintUtils; import java.io.ByteArrayOutputStream; import java.io.PipedInputStream; public class BTChipDongle implements BTChipConstants { private BTChipFirmware firmwareVersion; public enum OperationMode { WALLET(0x01), RELAXED_WALLET(0x02), SERVER(0x04), DEVELOPER(0x08); private int value; OperationMode(int value) { this.value = value; } public int getValue() { return value; } } public enum Feature { UNCOMPRESSED_KEYS(0x01), RFC6979(0x02), FREE_SIGHASHTYPE(0x04), NO_2FA_P2SH(0x08); private int value; Feature(int value) { this.value = value; } public int getValue() { return value; } } public enum UserConfirmation { NONE(0x00), KEYBOARD(0x01), KEYCARD_DEPRECATED(0x02), KEYCARD_SCREEN(0x03), KEYCARD(0x04), KEYCARD_NFC(0x05); private int value; UserConfirmation(int value) { this.value = value; } public int getValue() { return value; } } public class BTChipPublicKey { private byte[] publicKey; private String address; private byte[] chainCode; public BTChipPublicKey(byte[] publicKey, String address, byte[] chainCode) { this.publicKey = publicKey; this.address = address; this.chainCode = chainCode; } public byte[] getPublicKey() { return publicKey; } public String getAddress() { return address; } public byte[] getChainCode() { return chainCode; } @Override public String toString() { return String.format("Address %s public key %s chaincode %s", address, Dump.dump(publicKey), Dump.dump(chainCode)); } } public class BTChipSignature { private byte[] signature; private int yParity; public BTChipSignature(byte[] signature, int yParity) { this.signature = signature; this.yParity = yParity; } public byte[] getSignature() { return signature; } public int getYParity() { return yParity; } @Override public String toString() { return String.format("Signature %s y parity %s", Dump.dump(signature), yParity); } } public class BTChipFirmware { private int major; private int minor; private int patch; private boolean compressedKeys; public BTChipFirmware(int major, int minor, int patch, boolean compressedKeys) { this.major = major; this.minor = minor; this.patch = patch; this.compressedKeys = compressedKeys; } public int getMajor() { return major; } public int getMinor() { return minor; } public int getPatch() { return patch; } public boolean isCompressedKey() { return compressedKeys; } @Override public String toString() { return String.format("%s.%s.%s compressed keys %b", major, minor, patch, compressedKeys); } } public class BTChipInput { private byte[] value; private byte[] sequence; private boolean trusted; private boolean segwit; public BTChipInput(byte[] value, byte[] sequence, boolean trusted, boolean segwit) { this.value = value; this.sequence = sequence; this.trusted = trusted; this.segwit = segwit; } public byte[] getValue() { return value; } public boolean isTrusted() { return trusted; } public byte[] getSequence() { return sequence; } public boolean isSegwit() { return segwit; } @Override public String toString() { return String.format("Value %s trusted %b sequence %s segwit %b", Dump.dump(value), trusted, (sequence != null ? Dump.dump(sequence) : ""), segwit); } } public class BTChipOutput { private byte[] value; private UserConfirmation userConfirmation; public BTChipOutput(byte[] value, UserConfirmation userConfirmation) { this.value = value; this.userConfirmation = userConfirmation; } public byte[] getValue() { return value; } public boolean isConfirmationNeeded() { return (!userConfirmation.equals(UserConfirmation.NONE)); } public UserConfirmation getUserConfirmation() { return userConfirmation; } @Override public String toString() { return String.format("Value %s confirmation type %s", Dump.dump(value), userConfirmation); } } public class BTChipOutputKeycard extends BTChipOutput { private byte[] keycardIndexes; public BTChipOutputKeycard(byte[] value, UserConfirmation userConfirmation, byte[] keycardIndexes) { super(value, userConfirmation); this.keycardIndexes = keycardIndexes; } public byte[] getKeycardIndexes() { return keycardIndexes; } @Override public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append(super.toString()); buffer.append(" address indexes "); for (int i=0; i<keycardIndexes.length; i++) { buffer.append(i).append(" "); } return buffer.toString(); } } public class BTChipOutputKeycardScreen extends BTChipOutputKeycard { private byte[] screenInfo; public BTChipOutputKeycardScreen(byte[] value, UserConfirmation userConfirmation, byte[] keycardIndexes, byte[] screenInfo) { super(value, userConfirmation, keycardIndexes); this.screenInfo = screenInfo; } public byte[] getScreenInfo() { return screenInfo; } @Override public String toString() { return String.format("%s screen data %s", super.toString(), Dump.dump(screenInfo)); } } private BTChipTransport transport; private int lastSW; private boolean mUnderstandsMultipleOutputs; private static final int OK[] = { SW_OK }; private static final byte DUMMY[] = { 0 }; public BTChipDongle(BTChipTransport transport) { this.transport = transport; } public BTChipDongle(BTChipTransport transport, boolean understandsMultipleOutputs) { this.transport = transport; this.mUnderstandsMultipleOutputs = understandsMultipleOutputs; } public boolean understandsMultipleOutputs() { return mUnderstandsMultipleOutputs; } public BTChipTransport getTransport() { return transport; } public void setTransport(BTChipTransport transport) { this.transport = transport; } private byte[] exchange(byte[] apdu) throws BTChipException { byte[] response; try { response = transport.exchange(apdu).get(); } catch(Exception e) { throw new BTChipException("I/O error", e); } if (response.length < 2) { throw new BTChipException("Truncated response"); } lastSW = ((response[response.length - 2] & 0xff) << 8) | response[response.length - 1] & 0xff; byte[] result = new byte[response.length - 2]; System.arraycopy(response, 0, result, 0, response.length - 2); return result; } private byte[] exchangeCheck(byte[] apdu, int acceptedSW[]) throws BTChipException { byte[] response = exchange(apdu); if (acceptedSW == null) { return response; } for (int SW : acceptedSW) { if (lastSW == SW) { return response; } } throw new BTChipException("Invalid status", lastSW); } private byte[] exchangeApdu(byte cla, byte ins, byte p1, byte p2, byte[] data, int acceptedSW[]) throws BTChipException { byte[] apdu = new byte[data.length + 5]; apdu[0] = cla; apdu[1] = ins; apdu[2] = p1; apdu[3] = p2; apdu[4] = (byte)(data.length); System.arraycopy(data, 0, apdu, 5, data.length); return exchangeCheck(apdu, acceptedSW); } private byte[] exchangeApdu(byte cla, byte ins, byte p1, byte p2, int length, int acceptedSW[]) throws BTChipException { byte[] apdu = new byte[5]; apdu[0] = cla; apdu[1] = ins; apdu[2] = p1; apdu[3] = p2; apdu[4] = (byte)(length); return exchangeCheck(apdu, acceptedSW); } private byte[] exchangeApduSplit(byte cla, byte ins, byte p1, byte p2, byte[] data, int acceptedSW[]) throws BTChipException { int offset = 0; byte[] result = null; while (offset < data.length) { int blockLength = ((data.length - offset) > 255 ? 255 : data.length - offset); byte[] apdu = new byte[blockLength + 5]; apdu[0] = cla; apdu[1] = ins; apdu[2] = p1; apdu[3] = p2; apdu[4] = (byte)(blockLength); System.arraycopy(data, offset, apdu, 5, blockLength); result = exchangeCheck(apdu, acceptedSW); offset += blockLength; } return result; } private byte[] exchangeApduSplit2(byte cla, byte ins, byte p1, byte p2, byte[] data, byte[] data2, int acceptedSW[]) throws BTChipException { int offset = 0; byte[] result = null; int maxBlockSize = 255 - data2.length; while (offset < data.length) { int blockLength = ((data.length - offset) > maxBlockSize ? maxBlockSize : data.length - offset); boolean lastBlock = ((offset + blockLength) == data.length); byte[] apdu = new byte[blockLength + 5 + (lastBlock ? data2.length : 0)]; apdu[0] = cla; apdu[1] = ins; apdu[2] = p1; apdu[3] = p2; apdu[4] = (byte)(blockLength + (lastBlock ? data2.length : 0)); System.arraycopy(data, offset, apdu, 5, blockLength); if (lastBlock) { System.arraycopy(data2, 0, apdu, 5 + blockLength, data2.length); } result = exchangeCheck(apdu, acceptedSW); offset += blockLength; } return result; } public void verifyPin(byte[] pin) throws BTChipException { exchangeApdu(BTCHIP_CLA, BTCHIP_INS_VERIFY_PIN, (byte)0x00, (byte)0x00, pin, OK); } public int getVerifyPinRemainingAttempts() throws BTChipException { exchangeApdu(BTCHIP_CLA, BTCHIP_INS_VERIFY_PIN, (byte)0x80, (byte)0x00, DUMMY, null); if ((lastSW & 0xfff0) != 0x63c0) { throw new BTChipException("Invalid status", lastSW); } return (lastSW - 0x63c0); } public BTChipPublicKey getWalletPublicKey(String keyPath) throws BTChipException { byte data[] = BIP32Utils.splitPath(keyPath); byte response[] = exchangeApdu(BTCHIP_CLA, BTCHIP_INS_GET_WALLET_PUBLIC_KEY, (byte)0x00, (byte)0x00, data, OK); int offset = 0; byte publicKey[] = new byte[response[offset]]; offset++; System.arraycopy(response, offset, publicKey, 0, publicKey.length); offset += publicKey.length; byte address[] = new byte[response[offset]]; offset++; System.arraycopy(response, offset, address, 0, address.length); offset += address.length; byte chainCode[] = new byte[32]; System.arraycopy(response, offset, chainCode, 0, chainCode.length); return new BTChipPublicKey(publicKey, new String(address), chainCode); } public BTChipInput getTrustedInput(BitcoinTransaction transaction, long index, long sequence) throws BTChipException { ByteArrayOutputStream data = new ByteArrayOutputStream(); // Header BufferUtils.writeUint32BE(data, index); BufferUtils.writeBuffer(data, transaction.getVersion()); VarintUtils.write(data, transaction.getInputs().size()); exchangeApdu(BTCHIP_CLA, BTCHIP_INS_GET_TRUSTED_INPUT, (byte)0x00, (byte)0x00, data.toByteArray(), OK); // Each input for (BitcoinTransaction.BitcoinInput input : transaction.getInputs()) { data = new ByteArrayOutputStream(); BufferUtils.writeBuffer(data, input.getPrevOut()); VarintUtils.write(data, input.getScript().length); exchangeApdu(BTCHIP_CLA, BTCHIP_INS_GET_TRUSTED_INPUT, (byte)0x80, (byte)0x00, data.toByteArray(), OK); data = new ByteArrayOutputStream(); BufferUtils.writeBuffer(data, input.getScript()); exchangeApduSplit2(BTCHIP_CLA, BTCHIP_INS_GET_TRUSTED_INPUT, (byte)0x80, (byte)0x00, data.toByteArray(), input.getSequence(), OK); } // Number of outputs data = new ByteArrayOutputStream(); VarintUtils.write(data, transaction.getOutputs().size()); exchangeApdu(BTCHIP_CLA, BTCHIP_INS_GET_TRUSTED_INPUT, (byte)0x80, (byte)0x00, data.toByteArray(), OK); // Each output for (BitcoinTransaction.BitcoinOutput output : transaction.getOutputs()) { data = new ByteArrayOutputStream(); BufferUtils.writeBuffer(data, output.getAmount()); VarintUtils.write(data, output.getScript().length); exchangeApdu(BTCHIP_CLA, BTCHIP_INS_GET_TRUSTED_INPUT, (byte)0x80, (byte)0x00, data.toByteArray(), OK); data = new ByteArrayOutputStream(); BufferUtils.writeBuffer(data, output.getScript()); exchangeApduSplit(BTCHIP_CLA, BTCHIP_INS_GET_TRUSTED_INPUT, (byte)0x80, (byte)0x00, data.toByteArray(), OK); } // Locktime byte[] response = exchangeApdu(BTCHIP_CLA, BTCHIP_INS_GET_TRUSTED_INPUT, (byte)0x80, (byte)0x00, transaction.getLockTime(), OK); ByteArrayOutputStream sequenceBuf = new ByteArrayOutputStream(); BufferUtils.writeUint32LE(sequenceBuf, sequence); return new BTChipInput(response, sequenceBuf.toByteArray(), true, false); } public BTChipInput createInput(byte[] value, byte[] sequence, boolean trusted, boolean segwit) { return new BTChipInput(value, sequence, trusted, segwit); } public void startUntrustedTransction(boolean newTransaction, long inputIndex, BTChipInput usedInputList[], byte[] redeemScript, boolean segwit) throws BTChipException { // Start building a fake transaction with the passed inputs ByteArrayOutputStream data = new ByteArrayOutputStream(); BufferUtils.writeBuffer(data, BitcoinTransaction.DEFAULT_VERSION); VarintUtils.write(data, usedInputList.length); exchangeApdu(BTCHIP_CLA, BTCHIP_INS_HASH_INPUT_START, (byte)0x00, (newTransaction ? (segwit ? (byte)0x02 : (byte)0x00) : (byte)0x80), data.toByteArray(), OK); // Loop for each input long currentIndex = 0; for (BTChipInput input : usedInputList) { byte[] script = (currentIndex == inputIndex ? redeemScript : new byte[0]); data = new ByteArrayOutputStream(); data.write(input.isSegwit() ? (byte)0x02 : input.isTrusted() ? (byte)0x01 : (byte)0x00); if (input.isTrusted()) { // other inputs have constant length data.write(input.getValue().length); } BufferUtils.writeBuffer(data, input.getValue()); VarintUtils.write(data, script.length); exchangeApdu(BTCHIP_CLA, BTCHIP_INS_HASH_INPUT_START, (byte)0x80, (byte)0x00, data.toByteArray(), OK); data = new ByteArrayOutputStream(); BufferUtils.writeBuffer(data, script); BufferUtils.writeBuffer(data, input.getSequence()); exchangeApduSplit(BTCHIP_CLA, BTCHIP_INS_HASH_INPUT_START, (byte)0x80, (byte)0x00, data.toByteArray(), OK); currentIndex++; } } private BTChipOutput convertResponseToOutput(byte[] response) throws BTChipException { BTChipOutput result = null; byte[] value = new byte[response[0] & 0xff]; System.arraycopy(response, 1, value, 0, value.length); byte userConfirmationValue = response[1 + value.length]; if (userConfirmationValue == UserConfirmation.NONE.getValue()) { result = new BTChipOutput(value, UserConfirmation.NONE); } else if (userConfirmationValue == UserConfirmation.KEYBOARD.getValue()) { result = new BTChipOutput(value, UserConfirmation.KEYBOARD); } else if (userConfirmationValue == UserConfirmation.KEYCARD_DEPRECATED.getValue()) { byte[] keycardIndexes = new byte[response.length - 2 - value.length]; System.arraycopy(response, 2 + value.length, keycardIndexes, 0, keycardIndexes.length); result = new BTChipOutputKeycard(value, UserConfirmation.KEYCARD_DEPRECATED, keycardIndexes); } else if (userConfirmationValue == UserConfirmation.KEYCARD.getValue()) { byte keycardIndexesLength = response[2 + value.length]; byte[] keycardIndexes = new byte[keycardIndexesLength]; System.arraycopy(response, 3 + value.length, keycardIndexes, 0, keycardIndexes.length); result = new BTChipOutputKeycard(value, UserConfirmation.KEYCARD, keycardIndexes); } else if (userConfirmationValue == UserConfirmation.KEYCARD_NFC.getValue()) { byte keycardIndexesLength = response[2 + value.length]; byte[] keycardIndexes = new byte[keycardIndexesLength]; System.arraycopy(response, 3 + value.length, keycardIndexes, 0, keycardIndexes.length); result = new BTChipOutputKeycard(value, UserConfirmation.KEYCARD_NFC, keycardIndexes); } else if (userConfirmationValue == UserConfirmation.KEYCARD_SCREEN.getValue()) { byte keycardIndexesLength = response[2 + value.length]; byte[] keycardIndexes = new byte[keycardIndexesLength]; byte[] screenInfo = new byte[response.length - 3 - value.length - keycardIndexes.length]; System.arraycopy(response, 3 + value.length, keycardIndexes, 0, keycardIndexes.length); System.arraycopy(response, 3 + value.length + keycardIndexes.length, screenInfo, 0, screenInfo.length); result = new BTChipOutputKeycardScreen(value, UserConfirmation.KEYCARD_SCREEN, keycardIndexes, screenInfo); } if (result == null) { throw new BTChipException("Unsupported user confirmation method"); } return result; } public BTChipOutput finalizeInput(String outputAddress, String amount, String fees, String changePath) throws BTChipException { BTChipOutput result; ByteArrayOutputStream data = new ByteArrayOutputStream(); byte path[] = BIP32Utils.splitPath(changePath); data.write(outputAddress.length()); BufferUtils.writeBuffer(data, outputAddress.getBytes()); BufferUtils.writeUint64BE(data, CoinFormatUtils.toSatoshi(amount)); BufferUtils.writeUint64BE(data, CoinFormatUtils.toSatoshi(fees)); BufferUtils.writeBuffer(data, path); byte[] response = exchangeApdu(BTCHIP_CLA, BTCHIP_INS_HASH_INPUT_FINALIZE, (byte)0x02, (byte)0x00, data.toByteArray(), OK); result = convertResponseToOutput(response); return result; } public BTChipOutput finalizeInputFull(byte[] data, String changePath, boolean skipChangeCheck) throws BTChipException { BTChipOutput result = null; int offset = 0; byte[] response = null; byte[] path; boolean oldAPI = false; if (!skipChangeCheck) { if (changePath != null) { path = BIP32Utils.splitPath(changePath); exchangeApdu(BTCHIP_CLA, BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, (byte)0xFF, (byte)0x00, path, null); oldAPI = ((lastSW == SW_INCORRECT_P1_P2) || (lastSW == SW_WRONG_P1_P2)); } else { exchangeApdu(BTCHIP_CLA, BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, (byte)0xFF, (byte)0x00, new byte[1], null); oldAPI = ((lastSW == SW_INCORRECT_P1_P2) || (lastSW == SW_WRONG_P1_P2)); } } while (offset < data.length) { int blockLength = ((data.length - offset) > 255 ? 255 : data.length - offset); byte[] apdu = new byte[blockLength + 5]; apdu[0] = BTCHIP_CLA; apdu[1] = BTCHIP_INS_HASH_INPUT_FINALIZE_FULL; apdu[2] = ((offset + blockLength) == data.length ? (byte)0x80 : (byte)0x00); apdu[3] = (byte)0x00; apdu[4] = (byte)(blockLength); System.arraycopy(data, offset, apdu, 5, blockLength); response = exchangeCheck(apdu, OK); offset += blockLength; } if (oldAPI) { byte value = response[0]; if (value == UserConfirmation.NONE.getValue()) { result = new BTChipOutput(new byte[0], UserConfirmation.NONE); } else if (value == UserConfirmation.KEYBOARD.getValue()) { result = new BTChipOutput(new byte[0], UserConfirmation.KEYBOARD); } } else { result = convertResponseToOutput(response); } if (result == null) { throw new BTChipException("Unsupported user confirmation method"); } return result; } public BTChipOutput finalizeInputFull(byte[] data) throws BTChipException { return finalizeInputFull(data, null, false); } public BTChipOutput finalizeInputFull(byte[] data, String changePath) throws BTChipException { return finalizeInputFull(data, changePath, false); } public BTChipOutput finalizeInput(byte[] outputScript, String outputAddress, String amount, String fees, String changePath) throws BTChipException { // Try the new API first boolean oldAPI; byte[] path; if (changePath != null) { path = BIP32Utils.splitPath(changePath); exchangeApdu(BTCHIP_CLA, BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, (byte)0xFF, (byte)0x00, path, null); oldAPI = ((lastSW == SW_INCORRECT_P1_P2) || (lastSW == SW_WRONG_P1_P2)); } else { exchangeApdu(BTCHIP_CLA, BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, (byte)0xFF, (byte)0x00, new byte[1], null); oldAPI = ((lastSW == SW_INCORRECT_P1_P2) || (lastSW == SW_WRONG_P1_P2)); } if (oldAPI) { return finalizeInput(outputAddress, amount, fees, changePath); } else { return finalizeInputFull(outputScript, null, true); } } public byte[] untrustedHashSign(String privateKeyPath, String pin, long lockTime, byte sigHashType) throws BTChipException { ByteArrayOutputStream data = new ByteArrayOutputStream(); byte path[] = BIP32Utils.splitPath(privateKeyPath); BufferUtils.writeBuffer(data, path); data.write(pin.length()); BufferUtils.writeBuffer(data, pin.getBytes()); BufferUtils.writeUint32BE(data, lockTime); data.write(sigHashType); byte[] response = exchangeApdu(BTCHIP_CLA, BTCHIP_INS_HASH_SIGN, (byte)0x00, (byte)0x00, data.toByteArray(), OK); response[0] = (byte)0x30; return response; } public byte[] untrustedHashSign(String privateKeyPath, String pin) throws BTChipException { return untrustedHashSign(privateKeyPath, pin, 0, (byte)0x01); } public boolean shouldUseNewSigningApi() { try { if (this.firmwareVersion == null) this.getFirmwareVersion(); } catch (BTChipException e) { return false; } if (this.firmwareVersion.getMajor() > 0x2001) { // 0x2001 = Ledger 1.x, 0x3001 = Nano S return true; } else if (this.firmwareVersion.getMajor() == 0x2001 && this.firmwareVersion.getMinor() > 0) { return true; } else if (this.firmwareVersion.getMajor() == 0x2001 && this.firmwareVersion.getMinor() == 0 && this.firmwareVersion.getPatch() >= 2) { return true; } else { return false; } } public boolean signMessagePrepare(String path, byte[] message) throws BTChipException { ByteArrayOutputStream data = new ByteArrayOutputStream(); BufferUtils.writeBuffer(data, BIP32Utils.splitPath(path)); if (this.shouldUseNewSigningApi()) { // length has two bytes in Ledger 1.0.2+ data.write((byte)0); } data.write((byte)message.length); BufferUtils.writeBuffer(data, message); final byte p2 = (byte) (this.shouldUseNewSigningApi() ? 0x01 : 0x00); byte[] response = exchangeApdu(BTCHIP_CLA, BTCHIP_INS_SIGN_MESSAGE, (byte)0x00, p2, data.toByteArray(), OK); return (response[0] == (byte)0x01); } public BTChipSignature signMessageSign(byte[] pin) throws BTChipException { ByteArrayOutputStream data = new ByteArrayOutputStream(); if (pin == null) { data.write((byte)0); } else { data.write((byte)pin.length); BufferUtils.writeBuffer(data, pin); } final byte p2 = (byte) (this.shouldUseNewSigningApi() ? 0x01 : 0x00); byte[] response = exchangeApdu(BTCHIP_CLA, BTCHIP_INS_SIGN_MESSAGE, (byte)0x80, p2, data.toByteArray(), OK); int yParity = (response[0] & 0x0F); response[0] = (byte)0x30; return new BTChipSignature(response, yParity); } public BTChipFirmware getFirmwareVersion() throws BTChipException { byte[] response = exchangeApdu(BTCHIP_CLA, BTCHIP_INS_GET_FIRMWARE_VERSION, (byte)0x00, (byte)0x00, 0x00, OK); boolean compressedKeys = (response[0] == (byte)0x01); int major = ((response[1] & 0xff) << 8) | response[2] & 0xff; int minor = response[3] & 0xff; int patch = response[4] & 0xff; this.firmwareVersion = new BTChipFirmware(major, minor, patch, compressedKeys); return this.firmwareVersion; } public void setKeymapEncoding(byte[] keymapEncoding) throws BTChipException { ByteArrayOutputStream data = new ByteArrayOutputStream(); BufferUtils.writeBuffer(data, keymapEncoding); exchangeApdu(BTCHIP_CLA, BTCHIP_INS_SET_KEYMAP, (byte)0x00, (byte)0x00, data.toByteArray(), OK); } public boolean setup(OperationMode supportedOperationModes[], Feature features[], int keyVersion, int keyVersionP2SH, byte[] userPin, byte[] wipePin, byte[] keymapEncoding, byte[] seed, byte[] developerKey) throws BTChipException { int operationModeFlags = 0; int featuresFlags = 0; ByteArrayOutputStream data = new ByteArrayOutputStream(); for (OperationMode currentOperationMode : supportedOperationModes) { operationModeFlags |= currentOperationMode.getValue(); } for (Feature currentFeature : features) { featuresFlags |= currentFeature.getValue(); } data.write(operationModeFlags); data.write(featuresFlags); data.write(keyVersion); data.write(keyVersionP2SH); if ((userPin.length < 0x04) || (userPin.length > 0x20)) { throw new BTChipException("Invalid user PIN length"); } data.write(userPin.length); BufferUtils.writeBuffer(data, userPin); if (wipePin != null) { if (wipePin.length > 0x04) { throw new BTChipException("Invalid wipe PIN length"); } data.write(wipePin.length); BufferUtils.writeBuffer(data, wipePin); } else { data.write(0); } if (seed != null) { if ((seed.length < 32) || (seed.length > 64)) { throw new BTChipException("Invalid seed length"); } data.write(seed.length); BufferUtils.writeBuffer(data, seed); } else { data.write(0); } if (developerKey != null) { if (developerKey.length != 0x10) { throw new BTChipException("Invalid developer key"); } data.write(developerKey.length); BufferUtils.writeBuffer(data, developerKey); } else { data.write(0); } byte[] response = exchangeApdu(BTCHIP_CLA, BTCHIP_INS_SETUP, (byte)0x00, (byte)0x00, data.toByteArray(), OK); if (keymapEncoding != null) { setKeymapEncoding(keymapEncoding); } return (response[0] == (byte)0x01); } }