/* ******************************************************************************* * BTChip Bitcoin Hardware Wallet Java Card implementation * (c) 2013 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************* */ // TODO : Add storage of change address package com.btchip.applet.poc; import javacard.framework.APDU; import javacard.framework.Applet; import javacard.framework.CardRuntimeException; import javacard.framework.ISO7816; import javacard.framework.ISOException; import javacard.framework.JCSystem; import javacard.framework.OwnerPIN; import javacard.framework.Util; import javacard.security.ECPrivateKey; import javacard.security.ECPublicKey; import javacard.security.KeyPair; /** * Hardware Wallet applet * @author BTChip * */ public class BTChipPocApplet extends Applet { public BTChipPocApplet() { BCDUtils.init(); TC.init(); Crypto.init(); Transaction.init(); limits = new byte[LIMIT_LAST]; scratch255 = JCSystem.makeTransientByteArray((short)255, JCSystem.CLEAR_ON_DESELECT); transactionPin = new OwnerPIN(TRANSACTION_PIN_ATTEMPTS, TRANSACTION_PIN_SIZE); walletPin = new OwnerPIN(WALLET_PIN_ATTEMPTS, WALLET_PIN_SIZE); TC.ctxP[TC.P_TX_Z_USED] = TC.FALSE; setup = TC.FALSE; limitsSet = TC.FALSE; } protected static void writeIdleText() { short offset = Util.arrayCopyNonAtomic(TEXT_IDLE, (short)0, BTChipNFCForumApplet.FILE_DATA, BTChipNFCForumApplet.OFFSET_TEXT, (short)TEXT_IDLE.length); BTChipNFCForumApplet.writeHeader((short)(offset - BTChipNFCForumApplet.OFFSET_TEXT)); } protected static boolean isContactless() { return ((APDU.getProtocol() & APDU.PROTOCOL_MEDIA_MASK) == APDU.PROTOCOL_MEDIA_CONTACTLESS_TYPE_A); } private static void checkAccess() { if ((setup == TC.FALSE) || (setup != TC.TRUE)) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } if (!isContactless() && !walletPin.isValidated()) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } } private static void checkInterfaceConsistency() { // Check interface consistency - signature cannot go across interfaces if ((isContactless() && (TC.ctxP[TC.P_TX_Z_USED] != TC.FALSE)) || (!isContactless() && (TC.ctxP[TC.P_TX_Z_USED] != TC.TRUE))) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } } private static boolean isFirstSigned() { if (TC.ctxP[TC.P_TX_Z_USED] == TC.TRUE) { if (TC.ctxP[TC.P_TX_Z_FIRST_SIGNED] == TC.TRUE) { return true; } else if (TC.ctxP[TC.P_TX_Z_FIRST_SIGNED] == TC.FALSE) { return false; } else { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } } else if (TC.ctxP[TC.P_TX_Z_USED] == TC.FALSE) { if (TC.ctx[TC.TX_Z_FIRST_SIGNED] == TC.TRUE) { return true; } else if (TC.ctx[TC.TX_Z_FIRST_SIGNED] == TC.FALSE) { return false; } else { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } } else { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } return true; // happy compiler } private static void restoreState() { if (TC.ctxP[TC.P_TX_Z_USED] == TC.TRUE) { TC.ctx[TC.TX_B_TRANSACTION_STATE] = TC.ctxP[TC.P_TX_B_TRANSACTION_STATE]; Util.arrayCopyNonAtomic(TC.ctxP, TC.P_TX_A_AUTH_NONCE, TC.ctx, TC.TX_A_AUTH_NONCE, TC.TX_AUTH_CONTEXT_SIZE); Util.arrayCopyNonAtomic(TC.ctxP, TC.P_TX_A_AUTHORIZATION_HASH, TC.ctx, TC.TX_A_AUTHORIZATION_HASH, TC.SIZEOF_SHA256); TC.ctx[TC.TX_Z_HAS_CHANGE] = TC.ctxP[TC.P_TX_Z_HAS_CHANGE]; TC.ctx[TC.TX_Z_IS_P2SH] = TC.ctxP[TC.P_TX_Z_IS_P2SH]; } else if (TC.ctxP[TC.P_TX_Z_USED] == TC.FALSE) { } else { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } } private static void saveState() { if (TC.ctxP[TC.P_TX_Z_USED] == TC.TRUE) { TC.ctxP[TC.P_TX_B_TRANSACTION_STATE] = TC.ctx[TC.TX_B_TRANSACTION_STATE]; } else if (TC.ctxP[TC.P_TX_Z_USED] == TC.FALSE) { } else { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } } private static void verifyKeyChecksum(byte[] buffer, short offset, short length, byte[] scratch, short scratchOffset) { Crypto.digestScratch.doFinal(buffer, offset, (short)(length - 4), scratch, scratchOffset); Crypto.digestScratch.doFinal(scratch, scratchOffset, TC.SIZEOF_SHA256, scratch, scratchOffset); if (Util.arrayCompare(scratch, scratchOffset, buffer, (short)(offset + length - 4), (short)4) != (byte)0x00) { ISOException.throwIt(ISO7816.SW_WRONG_DATA); } } private static void handleGenerate(APDU apdu) throws ISOException { byte[] buffer = apdu.getBuffer(); byte p1 = buffer[ISO7816.OFFSET_P1]; byte p2 = buffer[ISO7816.OFFSET_P2]; short offset = ISO7816.OFFSET_CDATA; short scratchOffset = (short)0; ECPublicKey publicKey = null; short publicKeyOffset = (short)0; boolean prepare = ((p1 & P1_GENERATE_PREPARE) != 0); apdu.setIncomingAndReceive(); // PoC only supports generate + import, and does not generate integrity data or authorized addresses // Also, generation is always done for main net when used for change if (prepare) { if (((p1 & P1_GENERATE_PREPARE_DERIVE) != 0) || ((p1 & P1_GENERATE_PREPARE_HASH) != 0) || ((p1 & P1_GENERATE_PREPARE_UID) != 0) || ((p1 & P1_GENERATE_PROVIDE_AUTHORIZED_KEY) != 0) || ((p1 & P1_GENERATE_PREPARE_BASE58) != 0) || ((p1 & P1_GENERATE_PREPARE_BIN) == 0)) { ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED); } } WrappingKeyRepository.WrappingKey encryptionKey = WrappingKeyRepository.find(buffer[offset++], WrappingKeyRepository.ROLE_PRIVATE_KEY_ENCRYPTION); if (encryptionKey == null) { ISOException.throwIt(ISO7816.SW_WRONG_DATA); } // Drop signature keyset offset++; // Drop flags offset++; // Drop curve ID offset += 2; // Prepare the private key blob scratch255[scratchOffset++] = BLOB_MAGIC_PRIVATE_KEY_WITH_PUB; scratch255[scratchOffset++] = (byte)0x00; // flags RFU // skip curve ID scratchOffset += 2; // skip CRC scratchOffset += 2; Crypto.random.generateData(scratch255, scratchOffset, (short)2); // nonce scratchOffset += 2; // If importing, decode if (prepare) { publicKeyOffset = (short)(offset + PRIVATE_KEY_S_LENGTH); Util.arrayCopyNonAtomic(buffer, offset, scratch255, scratchOffset, (short)(PRIVATE_KEY_S_LENGTH + PUBLIC_KEY_W_LENGTH)); scratchOffset += (PRIVATE_KEY_S_LENGTH + PUBLIC_KEY_W_LENGTH); } else { // Otherwise, generate KeyPair keyPair = Crypto.generatePair(); ECPrivateKey privateKey = (ECPrivateKey)keyPair.getPrivate(); publicKey = (ECPublicKey)keyPair.getPublic(); privateKey.getS(scratch255, scratchOffset); scratchOffset += 32; // The component itself stays here to avoid stressing the flash even more publicKey.getW(scratch255, scratchOffset); scratchOffset += PUBLIC_KEY_W_LENGTH; } Crypto.random.generateData(scratch255, scratchOffset, (short)7); // nonce2 scratchOffset += 7; // Encrypt the blob ASAP encryptionKey.initCipher(true); Crypto.blobEncryptDecrypt.doFinal(scratch255, (short)0, scratchOffset, scratch255, (short)0); offset = 0; // Prepare the output // Public key buffer[offset++] = PUBLIC_KEY_W_LENGTH; if (publicKey == null) { Util.arrayCopyNonAtomic(buffer, publicKeyOffset, buffer, offset, PUBLIC_KEY_W_LENGTH); } else { publicKey.getW(buffer, offset); } offset += PUBLIC_KEY_W_LENGTH; // Blob buffer[offset++] = (byte)scratchOffset; Util.arrayCopyNonAtomic(scratch255, (short)0, buffer, offset, scratchOffset); offset += scratchOffset; // Derivation data and fake signature Util.arrayFillNonAtomic(buffer, offset, (short)(32 + 8), (byte)0x00); offset += (short)(32 + 8); apdu.setOutgoingAndSend((short)0, offset); } private static void handleTrustedInput(APDU apdu) throws ISOException { byte[] buffer = apdu.getBuffer(); byte p1 = buffer[ISO7816.OFFSET_P1]; byte dataOffset = (short)0; apdu.setIncomingAndReceive(); if (p1 == P1_TRUSTED_INPUT_FIRST) { // Early check WrappingKeyRepository.WrappingKey encryptionKey = WrappingKeyRepository.find(buffer[ISO7816.OFFSET_CDATA], WrappingKeyRepository.ROLE_TRUSTED_INPUT_ENCRYPTION); if (encryptionKey == null) { ISOException.throwIt(ISO7816.SW_WRONG_DATA); } TC.ctx[TC.TX_B_TRUSTED_INPUT_KEYSET] = buffer[ISO7816.OFFSET_CDATA]; Util.arrayCopyNonAtomic(buffer, (short)(ISO7816.OFFSET_CDATA + 1), TC.ctx, TC.TX_I_TRANSACTION_TARGET_INPUT, TC.SIZEOF_U32); TC.ctx[TC.TX_B_TRANSACTION_STATE] = Transaction.STATE_NONE; TC.ctx[TC.TX_B_TRUSTED_INPUT_PROCESSED] = (byte)0x00; TC.ctx[TC.TX_B_HASH_OPTION] = Transaction.HASH_FULL; dataOffset = 5; } else if (p1 != P1_TRUSTED_INPUT_NEXT) { ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); } short remainingData = (short)((short)(buffer[ISO7816.OFFSET_LC] & 0xff) - dataOffset); byte result = Transaction.parseTransaction(Transaction.PARSE_TRUSTED_INPUT, buffer, (short)(ISO7816.OFFSET_CDATA + dataOffset), remainingData); if (result == Transaction.RESULT_ERROR) { ISOException.throwIt(ISO7816.SW_WRONG_DATA); } else if (result == Transaction.RESULT_MORE) { return; } else if (result == Transaction.RESULT_FINISHED) { WrappingKeyRepository.WrappingKey encryptionKey = WrappingKeyRepository.find(TC.ctx[TC.TX_B_TRUSTED_INPUT_KEYSET], WrappingKeyRepository.ROLE_TRUSTED_INPUT_ENCRYPTION); short offset = 0; buffer[offset++] = BLOB_MAGIC_TRUSTED_INPUT; Crypto.random.generateData(buffer, offset, (short)3); offset += 3; Crypto.digestFull.doFinal(scratch255, (short)0, (short)0, scratch255, (short)0); Crypto.digestFull.doFinal(scratch255, (short)0, (short)32, buffer, offset); offset += 32; GenericBEHelper.swap(TC.SIZEOF_U32, buffer, offset, TC.ctx, TC.TX_I_TRANSACTION_TARGET_INPUT); offset += 4; Util.arrayCopyNonAtomic(TC.ctx, TC.TX_A_TRANSACTION_AMOUNT, buffer, offset, TC.SIZEOF_AMOUNT); offset += TC.SIZEOF_AMOUNT; encryptionKey.initCipher(true); // "sign", using the same cipher Crypto.blobEncryptDecrypt.doFinal(buffer, (short)0, offset, scratch255, (short)0); Util.arrayCopyNonAtomic(scratch255, (short)(offset - 8), buffer, offset, (short)8); offset += 8; apdu.setOutgoingAndSend((short)0, offset); } } private static void handleHashTransaction(APDU apdu) throws ISOException { byte[] buffer = apdu.getBuffer(); byte p1 = buffer[ISO7816.OFFSET_P1]; byte p2 = buffer[ISO7816.OFFSET_P2]; short dataOffset = (short)0; apdu.setIncomingAndReceive(); if (p1 == P1_HASH_TRANSACTION_FIRST) { // Initialize TC.ctx[TC.TX_B_TRANSACTION_STATE] = Transaction.STATE_NONE; TC.ctx[TC.TX_B_HASH_OPTION] = Transaction.HASH_BOTH; } else if (p1 != P1_HASH_TRANSACTION_NEXT) { ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); } if (p2 == P2_HASH_TRANSACTION_NEW_INPUT) { if (p1 == P1_HASH_TRANSACTION_FIRST) { checkAccess(); if (isContactless() && (limitsSet != TC.TRUE)) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } TC.ctxP[TC.P_TX_Z_USED] = (isContactless() ? TC.FALSE : TC.TRUE); if (TC.ctxP[TC.P_TX_Z_USED] == TC.TRUE) { TC.ctxP[TC.P_TX_Z_FIRST_SIGNED] = TC.TRUE; } else { TC.ctx[TC.TX_Z_FIRST_SIGNED] = TC.TRUE; } if (TC.ctxP[TC.P_TX_Z_USED] == TC.TRUE) { Crypto.random.generateData(TC.ctxP, TC.P_TX_A_AUTH_NONCE, TC.SIZEOF_NONCE); } else { Crypto.random.generateData(TC.ctx, TC.TX_A_AUTH_NONCE, TC.SIZEOF_NONCE); } dataOffset = (short)2; } } else if (p2 != P2_HASH_TRANSACTION_CONTINUE_INPUT) { ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); } checkInterfaceConsistency(); short remainingData = (short)((short)(buffer[ISO7816.OFFSET_LC] & 0xff) - dataOffset); byte result = Transaction.parseTransaction(Transaction.PARSE_SIGNATURE, buffer, (short)(ISO7816.OFFSET_CDATA + dataOffset), remainingData); if (result == Transaction.RESULT_ERROR) { ISOException.throwIt(ISO7816.SW_WRONG_DATA); } else if (result == Transaction.RESULT_MORE) { saveState(); return; } else if (result == Transaction.RESULT_FINISHED) { saveState(); return; } } private static short addTransactionOutput(byte[] buffer, short offset, byte[] hash160Address, short hash160Offset, byte[] amount, short amountOffset, boolean isP2sh) { byte[] pre = (isP2sh ? TRANSACTION_OUTPUT_SCRIPT_P2SH_PRE : TRANSACTION_OUTPUT_SCRIPT_PRE); byte[] post = (isP2sh ? TRANSACTION_OUTPUT_SCRIPT_P2SH_POST : TRANSACTION_OUTPUT_SCRIPT_POST); Uint64Helper.swap(buffer, offset, amount, amountOffset); offset += 8; offset = Util.arrayCopyNonAtomic(pre, (short)0, buffer, offset, (short)pre.length); offset = Util.arrayCopyNonAtomic(hash160Address, hash160Offset, buffer, offset, TC.SIZEOF_RIPEMD); offset = Util.arrayCopyNonAtomic(post, (short)0, buffer, offset, (short)post.length); return offset; } private static short writeAmount(short textOffset, short amountOffset, short addressOffset) { textOffset = BCDUtils.hexAmountToDisplayable(TC.ctx, amountOffset, BTChipNFCForumApplet.FILE_DATA, textOffset); BTChipNFCForumApplet.FILE_DATA[textOffset++] = TEXT_SPACE; textOffset = Util.arrayCopyNonAtomic(TEXT_BTC, (short)0, BTChipNFCForumApplet.FILE_DATA, textOffset, (short)TEXT_BTC.length); BTChipNFCForumApplet.FILE_DATA[textOffset++] = TEXT_SPACE; textOffset = Util.arrayCopyNonAtomic(TEXT_TO, (short)0, BTChipNFCForumApplet.FILE_DATA, textOffset, (short)TEXT_TO.length); // Recompute the key checksum in place as an additional sanity check Util.arrayCopyNonAtomic(TC.ctx, addressOffset, scratch255, (short)0, (short)(TC.SIZEOF_RIPEMD + 1)); Crypto.digestScratch.doFinal(scratch255, (short)0, (short)(TC.SIZEOF_RIPEMD + 1), scratch255, (short)(TC.SIZEOF_RIPEMD + 1)); Crypto.digestScratch.doFinal(scratch255, (short)(TC.SIZEOF_RIPEMD + 1), TC.SIZEOF_SHA256, scratch255, (short)(TC.SIZEOF_RIPEMD + 1)); textOffset = Base58.encode(scratch255, (short)0, (short)(TC.SIZEOF_RIPEMD + 1 + 4), BTChipNFCForumApplet.FILE_DATA, textOffset, scratch255, (short)100); return textOffset; } private static void handleHashOutput(APDU apdu) throws ISOException { byte[] buffer = apdu.getBuffer(); byte p1 = buffer[ISO7816.OFFSET_P1]; byte p2 = buffer[ISO7816.OFFSET_P2]; apdu.setIncomingAndReceive(); checkInterfaceConsistency(); restoreState(); if (TC.ctx[TC.TX_B_TRANSACTION_STATE] != Transaction.STATE_PRESIGN_READY) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } if (isFirstSigned()) { short length = (short)(buffer[ISO7816.OFFSET_LC] & 0xff); if (length < (short)(1 + 1 + 1 + 8 + 8)) { ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); } switch(p1) { case P1_HASH_OUTPUT_BASE58: break; default: ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); } short offset = (short)(ISO7816.OFFSET_CDATA); WrappingKeyRepository.WrappingKey encryptionKey = WrappingKeyRepository.find(buffer[offset++], WrappingKeyRepository.ROLE_PRIVATE_KEY_ENCRYPTION); if (encryptionKey == null) { ISOException.throwIt(ISO7816.SW_WRONG_DATA); } byte addressLength = buffer[offset++]; short changeKeyLength; short decodedLength = Base58.decode(buffer, offset, addressLength, scratch255, (short)0, scratch255, (short)100); if (decodedLength < 0) { ISOException.throwIt(ISO7816.SW_WRONG_DATA); } switch(scratch255[0]) { case KEY_VERSION: case KEY_VERSION_TESTNET: break; case KEY_VERSION_P2SH: case KEY_VERSION_P2SH_TESTNET: TC.ctx[TC.TX_Z_IS_P2SH] = TC.TRUE; break; default: ISOException.throwIt(ISO7816.SW_WRONG_DATA); } verifyKeyChecksum(scratch255, (short)0, decodedLength, scratch255, (short)100); Util.arrayCopyNonAtomic(scratch255, (short)0, TC.ctx, TC.TX_A_AUTH_OUTPUT_ADDRESS, (short)(TC.SIZEOF_RIPEMD + 1)); offset += addressLength; changeKeyLength = (short)(buffer[offset++] & 0xff); if (changeKeyLength != 0) { encryptionKey.initCipher(false); Crypto.blobEncryptDecrypt.doFinal(buffer, offset, changeKeyLength, scratch255, (short)0); if (scratch255[0] != BLOB_MAGIC_PRIVATE_KEY_WITH_PUB) { ISOException.throwIt(ISO7816.SW_WRONG_DATA); } // TODO : save the blob first (until the next validated transaction) to avoid an attack holding the change // We do not care about the private key, erase it immediately Util.arrayFillNonAtomic(scratch255, (short)0, OFFSET_PUBLIC_KEY_IN_PRIVATE_BLOB, (byte)0); } offset += changeKeyLength; Util.arrayCopyNonAtomic(buffer, offset, TC.ctx, TC.TX_A_AUTH_OUTPUT_AMOUNT, TC.SIZEOF_AMOUNT); offset += TC.SIZEOF_AMOUNT; Util.arrayCopyNonAtomic(buffer, offset, TC.ctx, TC.TX_A_AUTH_FEE_AMOUNT, TC.SIZEOF_AMOUNT); offset += TC.SIZEOF_AMOUNT; // Compute change == totalInputs - (amount + fees) Uint64Helper.add(scratch255, (short)240, TC.ctx, TC.TX_A_AUTH_OUTPUT_AMOUNT, TC.ctx, TC.TX_A_AUTH_FEE_AMOUNT); Uint64Helper.sub(TC.ctx, TC.TX_A_AUTH_CHANGE_AMOUNT, TC.ctx, TC.TX_A_TRANSACTION_AMOUNT, scratch255, (short)240); TC.ctx[TC.TX_Z_HAS_CHANGE] = (Uint64Helper.isZero(TC.ctx, TC.TX_A_AUTH_CHANGE_AMOUNT) ? TC.FALSE : TC.TRUE); // Enforce limits if (TC.ctxP[TC.P_TX_Z_USED] == TC.FALSE) { // Amount Uint64Helper.sub(scratch255, (short)200, limits, LIMIT_GLOBAL_AMOUNT, TC.ctx, TC.TX_A_AUTH_OUTPUT_AMOUNT); Util.arrayCopy(scratch255, (short)200, limits, LIMIT_GLOBAL_AMOUNT, TC.SIZEOF_AMOUNT); // Fees Uint64Helper.sub(scratch255, (short)200, limits, LIMIT_MAX_FEES, TC.ctx, TC.TX_A_AUTH_FEE_AMOUNT); // Change if (TC.ctx[TC.TX_Z_HAS_CHANGE] == TC.TRUE) { Uint64Helper.sub(scratch255, (short)200, limits, LIMIT_MAX_CHANGE, TC.ctx, TC.TX_A_AUTH_CHANGE_AMOUNT); } } if (TC.ctx[TC.TX_Z_HAS_CHANGE] == TC.TRUE) { if (changeKeyLength == (short)0) { ISOException.throwIt(ISO7816.SW_WRONG_DATA); } // Compute the change address - significant performance hit if not using a native RIPEMD160 Crypto.digestScratch.doFinal(scratch255, OFFSET_PUBLIC_KEY_IN_PRIVATE_BLOB, PUBLIC_KEY_W_LENGTH, scratch255, (short)0); TC.ctx[TC.TX_A_AUTH_CHANGE_ADDRESS] = KEY_VERSION; // force main net Crypto.hashRipemd32(scratch255, (short)0, TC.ctx, (short)(TC.TX_A_AUTH_CHANGE_ADDRESS + 1)); } if (TC.ctxP[TC.P_TX_Z_USED] == TC.TRUE) { Util.arrayCopy(TC.ctx, TC.TX_A_AUTH_NONCE, TC.ctxP, TC.P_TX_A_AUTH_NONCE, TC.TX_AUTH_CONTEXT_SIZE); TC.ctxP[TC.P_TX_Z_HAS_CHANGE] = TC.ctx[TC.TX_Z_HAS_CHANGE]; TC.ctxP[TC.P_TX_Z_IS_P2SH] = TC.ctx[TC.TX_Z_IS_P2SH]; } } short dataOffset = 0; short outOffset = 0; scratch255[dataOffset++] = ((TC.ctx[TC.TX_Z_HAS_CHANGE] == TC.TRUE) ? (byte)2 : (byte)1); dataOffset = addTransactionOutput(scratch255, dataOffset, TC.ctx, (short)(TC.TX_A_AUTH_OUTPUT_ADDRESS + 1), TC.ctx, TC.TX_A_AUTH_OUTPUT_AMOUNT, (TC.ctx[TC.TX_Z_IS_P2SH] == TC.TRUE)); if (TC.ctx[TC.TX_Z_HAS_CHANGE] == TC.TRUE) { dataOffset = addTransactionOutput(scratch255, dataOffset, TC.ctx, (short)(TC.TX_A_AUTH_CHANGE_ADDRESS + 1), TC.ctx, TC.TX_A_AUTH_CHANGE_AMOUNT, false); } // Update the main hash Crypto.digestFull.update(scratch255, (short)0, dataOffset); // Always return the output buffer[outOffset++] = (byte)dataOffset; Util.arrayCopyNonAtomic(scratch255, (short)0, buffer, outOffset, dataOffset); outOffset += dataOffset; if (isFirstSigned()) { buffer[outOffset++] = (byte)DUMMY_AUTHORIZATION_NFC.length; // dummy authorization given, for compatibility outOffset = Util.arrayCopyNonAtomic(DUMMY_AUTHORIZATION_NFC, (short)0, buffer, outOffset, (short)DUMMY_AUTHORIZATION_NFC.length); } else { buffer[outOffset++] = (byte)0; } // Update the authorization hash and check it if necessary Crypto.digestAuthorization.doFinal(TC.ctx, TC.TX_A_AUTH_NONCE, TC.TX_AUTH_CONTEXT_SIZE, scratch255, (short)0); if (isFirstSigned()) { Util.arrayCopyNonAtomic(scratch255, (short)0, TC.ctx, TC.TX_A_AUTHORIZATION_HASH, TC.SIZEOF_SHA256); TC.ctx[TC.TX_Z_FIRST_SIGNED] = TC.FALSE; if (TC.ctxP[TC.P_TX_Z_USED] == TC.TRUE) { Util.arrayCopyNonAtomic(scratch255, (short)0, TC.ctxP, TC.P_TX_A_AUTHORIZATION_HASH, TC.SIZEOF_SHA256); // First signature in contact mode - prepare the confirmation text and PIN TC.ctxP[TC.P_TX_Z_FIRST_SIGNED] = TC.FALSE; short textOffset = BTChipNFCForumApplet.OFFSET_TEXT; textOffset = Util.arrayCopyNonAtomic(TEXT_CONFIRM, (short)0, BTChipNFCForumApplet.FILE_DATA, textOffset, (short)TEXT_CONFIRM.length); textOffset = writeAmount(textOffset, TC.TX_A_AUTH_OUTPUT_AMOUNT, TC.TX_A_AUTH_OUTPUT_ADDRESS); BTChipNFCForumApplet.FILE_DATA[textOffset++] = TEXT_SPACE; textOffset = Util.arrayCopyNonAtomic(TEXT_FEES, (short)0, BTChipNFCForumApplet.FILE_DATA, textOffset, (short)TEXT_FEES.length); textOffset = BCDUtils.hexAmountToDisplayable(TC.ctx, TC.TX_A_AUTH_FEE_AMOUNT, BTChipNFCForumApplet.FILE_DATA, textOffset); BTChipNFCForumApplet.FILE_DATA[textOffset++] = TEXT_SPACE; textOffset = Util.arrayCopyNonAtomic(TEXT_BTC, (short)0, BTChipNFCForumApplet.FILE_DATA, textOffset, (short)TEXT_BTC.length); BTChipNFCForumApplet.FILE_DATA[textOffset++] = TEXT_COMMA; if (TC.ctx[TC.TX_Z_HAS_CHANGE] == TC.FALSE) { textOffset = Util.arrayCopyNonAtomic(TEXT_NO_CHANGE, (short)0, BTChipNFCForumApplet.FILE_DATA, textOffset, (short)TEXT_NO_CHANGE.length); } else { textOffset = Util.arrayCopyNonAtomic(TEXT_CHANGE, (short)0, BTChipNFCForumApplet.FILE_DATA, textOffset, (short)TEXT_CHANGE.length); BTChipNFCForumApplet.FILE_DATA[textOffset++] = TEXT_SPACE; textOffset = writeAmount(textOffset, TC.TX_A_AUTH_CHANGE_AMOUNT, TC.TX_A_AUTH_CHANGE_ADDRESS); } BTChipNFCForumApplet.FILE_DATA[textOffset++] = TEXT_CLOSE_P; BTChipNFCForumApplet.FILE_DATA[textOffset++] = TEXT_SPACE; textOffset = Util.arrayCopyNonAtomic(TEXT_PIN, (short)0, BTChipNFCForumApplet.FILE_DATA, textOffset, (short)TEXT_PIN.length); Crypto.random.generateData(scratch255, (short)0, TRANSACTION_PIN_SIZE); for (byte i=0; i<TRANSACTION_PIN_SIZE; i++) { scratch255[i] = (byte)((short)((scratch255[i] & 0xff)) % 10); scratch255[i] += (byte)'0'; } transactionPin.resetAndUnblock(); transactionPin.update(scratch255, (short)0, TRANSACTION_PIN_SIZE); textOffset = Util.arrayCopyNonAtomic(scratch255, (short)0, BTChipNFCForumApplet.FILE_DATA, textOffset, TRANSACTION_PIN_SIZE); BTChipNFCForumApplet.writeHeader((short)(textOffset - BTChipNFCForumApplet.OFFSET_TEXT)); } } else { if (Util.arrayCompare(scratch255, (short)0, TC.ctx, TC.TX_A_AUTHORIZATION_HASH, TC.SIZEOF_SHA256) != 0) { ISOException.throwIt(ISO7816.SW_WRONG_DATA); } } TC.ctx[TC.TX_B_TRANSACTION_STATE] = Transaction.STATE_SIGN_READY; saveState(); apdu.setOutgoingAndSend((short)0, outOffset); } private static void handleHashSign(APDU apdu) throws ISOException { byte[] buffer = apdu.getBuffer(); short offset = ISO7816.OFFSET_CDATA; apdu.setIncomingAndReceive(); checkInterfaceConsistency(); restoreState(); if (TC.ctx[TC.TX_B_TRANSACTION_STATE] != Transaction.STATE_SIGN_READY) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } WrappingKeyRepository.WrappingKey encryptionKey = WrappingKeyRepository.find(buffer[offset++], WrappingKeyRepository.ROLE_PRIVATE_KEY_ENCRYPTION); if (encryptionKey == null) { ISOException.throwIt(ISO7816.SW_WRONG_DATA); } short keyLength = (short)(buffer[offset++] & 0xff); encryptionKey.initCipher(false); Crypto.blobEncryptDecrypt.doFinal(buffer, offset, keyLength, scratch255, (short)0); if ((scratch255[0] != BLOB_MAGIC_PRIVATE_KEY_WITH_PUB) && (scratch255[0] != BLOB_MAGIC_PRIVATE_KEY)) { ISOException.throwIt(ISO7816.SW_WRONG_DATA); } offset += keyLength; offset++; // skip key authorization short authorizationLength = (short)(buffer[offset++] & 0xff); // Check the PIN if the transaction was started in contact mode if (TC.ctxP[TC.P_TX_Z_USED] == TC.TRUE) { // Clear the text BTChipPocApplet.writeIdleText(); if (!transactionPin.check(buffer, offset, (byte)authorizationLength)) { ISOException.throwIt(ISO7816.SW_WRONG_DATA); } } offset += authorizationLength; // Copy lockTime Uint32Helper.swap(scratch255, (short)100, buffer, offset); offset += 4; // Copy sigHashType byte sigHashType = buffer[offset++]; Uint32Helper.clear(scratch255, (short)104); scratch255[(short)104] = sigHashType; // Compute the signature Crypto.digestFull.doFinal(scratch255, (short)100, (short)8, scratch255, (short)100); Crypto.signTransientPrivate(scratch255, OFFSET_PRIVATE_KEY_IN_PRIVATE_BLOB, scratch255, (short)100, buffer, (short)0); short signatureSize = (short)((short)(buffer[1] & 0xff) + 2); buffer[signatureSize] = sigHashType; // TODO : reset transaction state saveState(); apdu.setOutgoingAndSend((short)0, (short)(signatureSize + 1)); } private static void handleSetup(APDU apdu) throws ISOException { byte[] buffer = apdu.getBuffer(); apdu.setIncomingAndReceive(); if ((setup == TC.TRUE) || (setup != TC.FALSE)) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } if (buffer[ISO7816.OFFSET_LC] != WALLET_PIN_SIZE) { ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); } walletPin.update(buffer, ISO7816.OFFSET_CDATA, WALLET_PIN_SIZE); Crypto.random.generateData(scratch255, (short)0, (short)16); WrappingKeyRepository.add((byte)0x40, WrappingKeyRepository.ROLE_TRUSTED_INPUT_ENCRYPTION, scratch255, (short)0); Crypto.random.generateData(buffer, (short)0, (short)16); WrappingKeyRepository.add((byte)0x02, WrappingKeyRepository.ROLE_PRIVATE_KEY_ENCRYPTION, buffer, (short)0); apdu.setOutgoingAndSend((short)0, (short)16); setup = TC.TRUE; } private static void handleUnlock(APDU apdu) throws ISOException { byte[] buffer = apdu.getBuffer(); apdu.setIncomingAndReceive(); if ((setup == TC.FALSE) || (setup != TC.TRUE)) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } if (buffer[ISO7816.OFFSET_LC] != WALLET_PIN_SIZE) { ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); } if (!walletPin.check(buffer, ISO7816.OFFSET_CDATA, WALLET_PIN_SIZE)) { ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); } } private static void handleGetContactlessLimit(APDU apdu) throws ISOException { if ((setup == TC.FALSE) || (setup != TC.TRUE)) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } Util.arrayCopyNonAtomic(limits, (short)0, scratch255, (short)0, LIMIT_LAST); apdu.setOutgoingAndSend((short)0, LIMIT_LAST); } private static void handleSetContactlessLimit(APDU apdu) throws ISOException { byte[] buffer = apdu.getBuffer(); apdu.setIncomingAndReceive(); if (isContactless()) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } if (buffer[ISO7816.OFFSET_LC] != LIMIT_LAST) { ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); } Util.arrayCopy(buffer, ISO7816.OFFSET_CDATA, limits, (short)0, LIMIT_LAST); if (limitsSet != TC.TRUE) { limitsSet = TC.TRUE; } } public static void clearScratch() { Util.arrayFillNonAtomic(scratch255, (short)0, (short)scratch255.length, (byte)0x00); } public void process(APDU apdu) throws ISOException { if (selectingApplet()) { return; } byte[] buffer = apdu.getBuffer(); if (buffer[ISO7816.OFFSET_CLA] == CLA_BTC) { clearScratch(); try { switch(buffer[ISO7816.OFFSET_INS]) { case INS_SETUP: handleSetup(apdu); break; case INS_UNLOCK: handleUnlock(apdu); break; case INS_GET_CONTACTLESS_LIMIT: handleGetContactlessLimit(apdu); break; case INS_SET_CONTACTLESS_LIMIT: checkAccess(); handleSetContactlessLimit(apdu); break; case INS_GENERATE: checkAccess(); handleGenerate(apdu); break; case INS_GET_TRUSTED_INPUT: checkAccess(); handleTrustedInput(apdu); break; case INS_UNTRUSTED_HASH_START: handleHashTransaction(apdu); break; case INS_UNTRUSTED_HASH_FINALIZE: handleHashOutput(apdu); break; case INS_UNTRUSTED_HASH_SIGN: handleHashSign(apdu); break; default: ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); } } catch(Exception e) { //e.printStackTrace(); // Abort the current transaction if an exception is thrown TC.clear(); if (e instanceof CardRuntimeException) { throw ((CardRuntimeException)e); } else { ISOException.throwIt(ISO7816.SW_UNKNOWN); } } finally { clearScratch(); } return; } } public static void install (byte bArray[], short bOffset, byte bLength) throws ISOException { new BTChipPocApplet().register(bArray, (short)(bOffset + 1), bArray[bOffset]); } private static final byte TRANSACTION_PIN_ATTEMPTS = (byte)1; private static final byte TRANSACTION_PIN_SIZE = (byte)4; private static final byte WALLET_PIN_ATTEMPTS = (byte)3; private static final byte WALLET_PIN_SIZE = (byte)8; private static final byte TEXT_IDLE[] = { 'N', 'o', ' ', 'p', 'e', 'n', 'd', 'i', 'n', 'g', ' ', 't', 'r', 'a', 'n', 's', 'f', 'e', 'r' }; private static final byte TEXT_CONFIRM[] = { 'C', 'o', 'n', 'f', 'i', 'r', 'm', ' ', 't', 'r', 'a', 'n', 's', 'f', 'e', 'r', ' ', 'o', 'f', ' ' }; private static final byte TEXT_BTC[] = { 'B', 'T', 'C' }; private static final byte TEXT_TO[] = { 't', 'o', ' ' }; private static final byte TEXT_FEES[] = { '(', 'f', 'e', 'e', 's', ' ' }; private static final byte TEXT_NO_CHANGE[] = { 'n', 'o', ' ', 'c', 'h', 'a', 'n', 'g', 'e' }; private static final byte TEXT_CHANGE[] = { 'c', 'h', 'a', 'n', 'g', 'e' }; private static final byte TEXT_PIN[] = { 'w', 'i', 't', 'h', ' ', 'P', 'I', 'N', ' ' }; private static final byte TEXT_CLOSE_P = ')'; private static final byte TEXT_SPACE = ' '; private static final byte TEXT_COMMA = ','; private static final byte DUMMY_AUTHORIZATION_NFC[] = { (byte)'N', (byte)'F', (byte)'C' }; private static final byte TRANSACTION_OUTPUT_SCRIPT_PRE[] = { (byte)0x19, (byte)0x76, (byte)0xA9, (byte)0x14 }; // script length, OP_DUP, OP_HASH160, address length private static final byte TRANSACTION_OUTPUT_SCRIPT_POST[] = { (byte)0x88, (byte)0xAC }; // OP_EQUALVERIFY, OP_CHECKSIG private static final byte TRANSACTION_OUTPUT_SCRIPT_P2SH_PRE[] = { (byte)0x17, (byte)0xA9, (byte)0x14 }; // script length, OP_HASH160, address length private static final byte TRANSACTION_OUTPUT_SCRIPT_P2SH_POST[] = { (byte)0x87 }; // OP_EQUAL private static final byte KEY_VERSION_P2SH = (byte)0x05; private static final byte KEY_VERSION_P2SH_TESTNET = (byte)0xC4; private static final byte KEY_VERSION_PRIVATE = (byte)0x80; private static final byte KEY_VERSION = (byte)0x00; private static final byte KEY_VERSION_TESTNET = (byte)0x6F; private static final byte PUBLIC_KEY_W_LENGTH = 65; private static final byte PRIVATE_KEY_S_LENGTH = 32; private static final byte OFFSET_PRIVATE_KEY_IN_PRIVATE_BLOB = (short)(1 + 1 + 2 + 2 + 2); private static final byte OFFSET_PUBLIC_KEY_IN_PRIVATE_BLOB = (short)(1 + 1 + 2 + 2 + 2 + PRIVATE_KEY_S_LENGTH); private static final byte CLA_BTC = (byte)0xE0; private static final byte INS_GENERATE = (byte)0x20; private static final byte INS_GET_TRUSTED_INPUT = (byte)0x42; private static final byte INS_UNTRUSTED_HASH_START = (byte)0x44; private static final byte INS_UNTRUSTED_HASH_FINALIZE = (byte)0x46; private static final byte INS_UNTRUSTED_HASH_SIGN = (byte)0x48; private static final byte INS_SETUP = (byte)0xA0; private static final byte INS_UNLOCK = (byte)0xA2; private static final byte INS_SET_CONTACTLESS_LIMIT = (byte)0xA4; private static final byte INS_GET_CONTACTLESS_LIMIT = (byte)0xA6; private static final byte P1_GENERATE_PREPARE = (byte)0x80; private static final byte P1_GENERATE_PROVIDE_AUTHORIZED_KEY = (byte)0x01; private static final byte P1_GENERATE_PREPARE_BASE58 = (byte)0x02; private static final byte P1_GENERATE_PREPARE_HASH = (byte)0x04; private static final byte P1_GENERATE_PREPARE_DERIVE = (byte)0x08; private static final byte P1_GENERATE_PREPARE_UID = (byte)0x10; private static final byte P1_GENERATE_PREPARE_BIN = (byte)0x20; private static final byte P1_TRUSTED_INPUT_FIRST = (byte)0x00; private static final byte P1_TRUSTED_INPUT_NEXT = (byte)0x80; private static final byte P1_HASH_TRANSACTION_FIRST = (byte)0x00; private static final byte P1_HASH_TRANSACTION_NEXT = (byte)0x80; private static final byte P2_HASH_TRANSACTION_NEW_INPUT = (byte)0x00; private static final byte P2_HASH_TRANSACTION_CONTINUE_INPUT = (byte)0x80; private static final byte P1_HASH_OUTPUT_HASH160 = (byte)0x01; private static final byte P1_HASH_OUTPUT_BASE58 = (byte)0x02; private static final byte P1_HASH_OUTPUT_AUTHORIZED_ADDRESS = (byte)0x03; private static final byte P1_HASH_OUTPUT_HASH160_P2SH = (byte)0x04; public static final byte BLOB_MAGIC_PRIVATE_KEY = (byte)0x01; public static final byte BLOB_MAGIC_PRIVATE_KEY_WITH_PUB = (byte)0x11; public static final byte BLOB_MAGIC_ENCODED_ADDRESS = (byte)0x21; public static final byte BLOB_MAGIC_TRUSTED_INPUT = (byte)0x31; private static final byte LIMIT_GLOBAL_AMOUNT = (byte)0; private static final byte LIMIT_MAX_FEES = (byte)(LIMIT_GLOBAL_AMOUNT + TC.SIZEOF_AMOUNT); private static final byte LIMIT_MAX_CHANGE = (byte)(LIMIT_MAX_FEES + TC.SIZEOF_AMOUNT); private static final byte LIMIT_LAST = (byte)(LIMIT_MAX_CHANGE + TC.SIZEOF_AMOUNT); public static byte[] scratch255; private static OwnerPIN transactionPin; private static OwnerPIN walletPin; private static byte setup; private static byte limitsSet; private static byte[] limits; }