/*
*******************************************************************************
* 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/>.
*******************************************************************************
*/
package com.btchip.applet.poc;
import javacard.framework.JCSystem;
import javacard.framework.Util;
/**
* Bitcoin transaction parsing
* @author BTChip
*
*/
public class Transaction {
public static void init() {
h = JCSystem.makeTransientShortArray((short)2, JCSystem.CLEAR_ON_DESELECT);
}
private static void consumeTransaction(byte buffer[], short length) {
if ((TC.ctx[TC.TX_B_HASH_OPTION] & HASH_FULL) != 0) {
Crypto.digestFull.update(buffer, h[CURRENT], length);
}
if ((TC.ctx[TC.TX_B_HASH_OPTION] & HASH_AUTHORIZATION) != 0) {
Crypto.digestAuthorization.update(buffer, h[CURRENT], length);
}
h[REMAINING] -= length;
h[CURRENT] += length;
}
private static boolean parseVarint(byte[] buffer, byte[] target, short targetOffset) {
if (h[REMAINING] < (short)1) {
return false;
}
short firstByte = (short)(buffer[h[CURRENT]] & 0xff);
if (firstByte < (short)0xfd) {
Uint32Helper.setByte(target, targetOffset, (byte)firstByte);
consumeTransaction(buffer, (short)1);
}
else
if (firstByte == (short)0xfd) {
consumeTransaction(buffer, (short)1);
if (h[REMAINING] < (short)2) {
return false;
}
Uint32Helper.setShort(target, targetOffset, buffer[(short)(h[CURRENT] + 1)], buffer[h[CURRENT]]);
consumeTransaction(buffer, (short)2);
}
else
if (firstByte == (short)0xfe) {
consumeTransaction(buffer, (short)1);
if (h[REMAINING] < (short)2) {
return false;
}
Uint32Helper.setInt(target, targetOffset, buffer[(short)(h[CURRENT] + 3)], buffer[(short)(h[CURRENT] + 2)], buffer[(short)(h[CURRENT] + 1)], buffer[h[CURRENT]]);
consumeTransaction(buffer, (short)4);
}
else {
return false;
}
return true;
}
public static byte parseTransaction(byte parseMode, byte buffer[], short offset, short remaining) {
h[CURRENT] = offset;
h[REMAINING] = remaining;
for (;;) {
if (TC.ctx[TC.TX_B_TRANSACTION_STATE] == STATE_NONE) {
Uint32Helper.clear(TC.ctx, TC.TX_I_REMAINING_IO);
Uint32Helper.clear(TC.ctx, TC.TX_I_CURRENT_IO);
Uint32Helper.clear(TC.ctx, TC.TX_I_SCRIPT_REMAINING);
Uint64Helper.clear(TC.ctx, TC.TX_A_TRANSACTION_AMOUNT);
Crypto.digestFull.reset();
Crypto.digestAuthorization.reset();
// Parse the beginning of the transaction
// Version
if (h[REMAINING] < (short)4) {
return RESULT_ERROR;
}
consumeTransaction(buffer, (short)4);
// Number of inputs
if (!parseVarint(buffer, TC.ctx, TC.TX_I_REMAINING_IO)) {
return RESULT_ERROR;
}
TC.ctx[TC.TX_B_TRANSACTION_STATE] = STATE_DEFINED_WAIT_INPUT;
}
if (TC.ctx[TC.TX_B_TRANSACTION_STATE] == STATE_DEFINED_WAIT_INPUT) {
if (Uint32Helper.isZero(TC.ctx, TC.TX_I_REMAINING_IO)) {
// No more inputs to hash, move forward
TC.ctx[TC.TX_B_TRANSACTION_STATE] = STATE_INPUT_HASHING_DONE;
continue;
}
if (h[REMAINING] < (short)1) {
// No more data to read, ok
return RESULT_MORE;
}
// Proceed with the next input
if (parseMode == PARSE_TRUSTED_INPUT) {
if (h[REMAINING] < (short)36) { // prevout : 32 hash + 4 index
return RESULT_ERROR;
}
consumeTransaction(buffer, (short)36);
}
if (parseMode == PARSE_SIGNATURE) {
// Expect the trusted input keyset and trusted input length
if (h[REMAINING] < (short)2) {
return RESULT_ERROR;
}
byte trustedInputKeyset = buffer[h[CURRENT]];
short trustedInputLength = (short)(buffer[(short)(h[CURRENT] + 1)] & 0xff);
if (trustedInputLength > BTChipPocApplet.scratch255.length) {
return RESULT_ERROR;
}
if (h[REMAINING] < (short)(2 + trustedInputLength)) {
return RESULT_ERROR;
}
if (buffer[(short)(h[CURRENT] + 2)] != BTChipPocApplet.BLOB_MAGIC_TRUSTED_INPUT) {
return RESULT_ERROR;
}
WrappingKeyRepository.WrappingKey encryptionKey = WrappingKeyRepository.find(trustedInputKeyset, WrappingKeyRepository.ROLE_TRUSTED_INPUT_ENCRYPTION);
if (encryptionKey == null) {
return RESULT_ERROR;
}
// Check the "signature"
encryptionKey.initCipher(true);
Crypto.blobEncryptDecrypt.doFinal(buffer, (short)(h[CURRENT] + 2), (short)(trustedInputLength - 8), BTChipPocApplet.scratch255, (short)0);
if (Util.arrayCompare(buffer, (short)(h[CURRENT] + 2 + trustedInputLength - 8), BTChipPocApplet.scratch255, (short)(trustedInputLength - 16), (short)8) != 0) {
return RESULT_ERROR;
}
// Update the amount
Uint64Helper.swap(BTChipPocApplet.scratch255, (short)0, buffer, (short)(h[CURRENT] + 2 + 40));
Uint64Helper.add(TC.ctx, TC.TX_A_TRANSACTION_AMOUNT, BTChipPocApplet.scratch255, (short)0);
// Update the hash with prevout data
short savedCurrent = h[CURRENT];
short savedRemaining = h[REMAINING];
h[CURRENT] += (short)(4 + 2);
consumeTransaction(buffer, (short)36);
h[CURRENT] = (short)(savedCurrent + 2 + trustedInputLength);
h[REMAINING] = (short)(savedRemaining - 2 - trustedInputLength);
// Do not include the input script length + value in the authentication hash
TC.ctx[TC.TX_B_HASH_OPTION] = HASH_FULL;
}
// Read the script length
if (!parseVarint(buffer, TC.ctx, TC.TX_I_SCRIPT_REMAINING)) {
return RESULT_ERROR;
}
TC.ctx[TC.TX_B_TRANSACTION_STATE] = STATE_INPUT_HASHING_IN_PROGRESS_INPUT_SCRIPT;
}
if (TC.ctx[TC.TX_B_TRANSACTION_STATE] == STATE_INPUT_HASHING_IN_PROGRESS_INPUT_SCRIPT) {
if (h[REMAINING] < (short)1) {
// No more data to read, ok
return RESULT_MORE;
}
if (Uint32Helper.isZero(TC.ctx,TC.TX_I_SCRIPT_REMAINING)) {
if (parseMode == PARSE_SIGNATURE) {
// Restore dual hash for signature + authentication
TC.ctx[TC.TX_B_HASH_OPTION] = HASH_BOTH;
}
// Sequence
if (h[REMAINING] < (short)4) {
return RESULT_ERROR;
}
// TODO : enforce sequence
consumeTransaction(buffer, (short)4);
// Move to next input
Uint32Helper.decrease(TC.ctx, TC.TX_I_REMAINING_IO);
Uint32Helper.increase(TC.ctx, TC.TX_I_CURRENT_IO);
TC.ctx[TC.TX_B_TRANSACTION_STATE] = STATE_DEFINED_WAIT_INPUT;
continue;
}
short scriptRemaining = Uint32Helper.getU8(TC.ctx, TC.TX_I_SCRIPT_REMAINING);
short dataAvailable = (h[REMAINING] > scriptRemaining ? scriptRemaining : h[REMAINING]);
if (dataAvailable == 0) {
return RESULT_MORE;
}
consumeTransaction(buffer, dataAvailable);
Uint32Helper.setByte(BTChipPocApplet.scratch255, (short)0, (byte)dataAvailable);
Uint32Helper.sub(TC.ctx, TC.TX_I_SCRIPT_REMAINING, BTChipPocApplet.scratch255, (short)0);
}
if (TC.ctx[TC.TX_B_TRANSACTION_STATE] == STATE_INPUT_HASHING_DONE) {
if (parseMode == PARSE_SIGNATURE) {
// inputs have been prepared, stop the parsing here
TC.ctx[TC.TX_B_TRANSACTION_STATE] = STATE_PRESIGN_READY;
continue;
}
if (h[REMAINING] < (short)1) {
// No more data to read, ok
return RESULT_MORE;
}
// Number of outputs
if (!parseVarint(buffer, TC.ctx, TC.TX_I_REMAINING_IO)) {
return RESULT_ERROR;
}
Uint32Helper.clear(TC.ctx, TC.TX_I_CURRENT_IO);
TC.ctx[TC.TX_B_TRANSACTION_STATE] = STATE_DEFINED_WAIT_OUTPUT;
}
if (TC.ctx[TC.TX_B_TRANSACTION_STATE] == STATE_DEFINED_WAIT_OUTPUT) {
if (Uint32Helper.isZero(TC.ctx, TC.TX_I_REMAINING_IO)) {
// No more outputs to hash, move forward
TC.ctx[TC.TX_B_TRANSACTION_STATE] = STATE_OUTPUT_HASHING_DONE;
continue;
}
if (h[REMAINING] < (short)1) {
// No more data to read, ok
return RESULT_MORE;
}
// Amount
if (h[REMAINING] < (short)8) {
return RESULT_ERROR;
}
if ((parseMode == PARSE_TRUSTED_INPUT) && (
Util.arrayCompare(TC.ctx, TC.TX_I_CURRENT_IO, TC.ctx, TC.TX_I_TRANSACTION_TARGET_INPUT, TC.SIZEOF_U32) == 0)) {
// Save the amount
Util.arrayCopyNonAtomic(buffer, h[CURRENT], TC.ctx, TC.TX_A_TRANSACTION_AMOUNT, TC.SIZEOF_AMOUNT);
TC.ctx[TC.TX_B_TRUSTED_INPUT_PROCESSED] = (byte)0x01;
}
consumeTransaction(buffer, (short)8);
// Read the script length
if (!parseVarint(buffer, TC.ctx, TC.TX_I_SCRIPT_REMAINING)) {
return RESULT_ERROR;
}
TC.ctx[TC.TX_B_TRANSACTION_STATE] = STATE_OUTPUT_HASHING_IN_PROGRESS_OUTPUT_SCRIPT;
}
if (TC.ctx[TC.TX_B_TRANSACTION_STATE] == STATE_OUTPUT_HASHING_IN_PROGRESS_OUTPUT_SCRIPT) {
if (h[REMAINING] < (short)1) {
// No more data to read, ok
return RESULT_MORE;
}
if (Uint32Helper.isZero(TC.ctx,TC.TX_I_SCRIPT_REMAINING)) {
// Move to next output
Uint32Helper.decrease(TC.ctx, TC.TX_I_REMAINING_IO);
Uint32Helper.increase(TC.ctx, TC.TX_I_CURRENT_IO);
TC.ctx[TC.TX_B_TRANSACTION_STATE] = STATE_DEFINED_WAIT_OUTPUT;
continue;
}
short scriptRemaining = Uint32Helper.getU8(TC.ctx, TC.TX_I_SCRIPT_REMAINING);
short dataAvailable = (h[REMAINING] > scriptRemaining ? scriptRemaining : h[REMAINING]);
if (dataAvailable == 0) {
return RESULT_MORE;
}
consumeTransaction(buffer, dataAvailable);
Uint32Helper.setByte(BTChipPocApplet.scratch255, (short)0, (byte)dataAvailable);
Uint32Helper.sub(TC.ctx, TC.TX_I_SCRIPT_REMAINING, BTChipPocApplet.scratch255, (short)0);
}
if (TC.ctx[TC.TX_B_TRANSACTION_STATE] == STATE_OUTPUT_HASHING_DONE) {
if (h[REMAINING] < (short)1) {
// No more data to read, ok
return RESULT_MORE;
}
// Locktime
if (h[REMAINING] < (short)4) {
return RESULT_ERROR;
}
consumeTransaction(buffer, (short)4);
TC.ctx[TC.TX_B_TRANSACTION_STATE] = STATE_PARSED;
}
if (TC.ctx[TC.TX_B_TRANSACTION_STATE] == STATE_PARSED) {
return RESULT_FINISHED;
}
if (TC.ctx[TC.TX_B_TRANSACTION_STATE] == STATE_PRESIGN_READY) {
return RESULT_FINISHED;
}
if (TC.ctx[TC.TX_B_TRANSACTION_STATE] == STATE_SIGN_READY) {
return RESULT_FINISHED;
}
}
}
private static short[] h;
private static final byte CURRENT = (byte)0;
private static final byte REMAINING = (byte)1;
public static final byte STATE_NONE = (byte)0x00;
public static final byte STATE_DEFINED_WAIT_INPUT = (byte)0x01;
public static final byte STATE_INPUT_HASHING_IN_PROGRESS_INPUT_SCRIPT = (byte)0x02;
public static final byte STATE_INPUT_HASHING_DONE = (byte)0x03;
public static final byte STATE_DEFINED_WAIT_OUTPUT = (byte)0x04;
public static final byte STATE_OUTPUT_HASHING_IN_PROGRESS_OUTPUT_SCRIPT = (byte)0x05;
public static final byte STATE_OUTPUT_HASHING_DONE = (byte)0x06;
public static final byte STATE_PARSED = (byte)0x07;
public static final byte STATE_PRESIGN_READY = (byte)0x08;
public static final byte STATE_SIGN_READY = (byte)0x09;
public static final byte HASH_NONE = (byte)0x00;
public static final byte HASH_FULL = (byte)0x01;
public static final byte HASH_AUTHORIZATION = (byte)0x02;
public static final byte HASH_BOTH = (byte)0x03;
public static final byte PARSE_TRUSTED_INPUT = (byte)0x01;
public static final byte PARSE_SIGNATURE = (byte)0x02;
public static final byte RESULT_FINISHED = (byte)0x13;
public static final byte RESULT_ERROR = (byte)0x79;
public static final byte RESULT_MORE = (byte)0x00;
}