/// <summary> /// <P>Performs the unsigned debit, subtracting the debit amount from /// the user's balance and storing the new, unsigned account data on the /// user's iButton. The debit amount can be set using /// <code>transaction.setParameter(SHADebit.DEBIT_AMOUNT, 50)</code>, /// where the value is in units of cents (i.e. for 1 dollar, use 100).</P> /// /// <P><B>Flow of action: </B> /// <ul> /// <li> Read the account data from user </li> /// <li> Extract account balance </li> /// <li> Assert balance greater than debit amount </li> /// <li> Debit money from balance </li> /// <li> Insert the new balance </li> /// <li> Write the account data to the user </li> /// </ul></P> /// /// <P>If previous steps have been executed, all "Read" commands on /// the user are reading from cached data.</P> /// </summary> /// <param name="user"> SHAiButtonUser upon which the transaction occurs. /// </param> /// <returns> <code>true</code> if and only if the user has enough in the /// account balance to perform the requested debit AND the account data /// has been written to the button. /// </returns> /// <seealso cref= SHAiButtonUser#readAccountData(byte[],int) </seealso> /// <seealso cref= SHAiButtonUser#writeAccountData(byte[],int) </seealso> /// <seealso cref= #getLastError() </seealso> public override bool executeTransaction(SHAiButtonUser user, bool verifySuccess) { lock (this) { //clear any error this.lastError = NO_ERROR; //init local vars //holds the working copy of account data byte[] accountData = this.executeTransaction_accountData; //holds the backup copy of account data before writing byte[] oldAcctData = this.executeTransaction_oldAcctData; //holds the account data read back for checking byte[] newAcctData = this.executeTransaction_newAcctData; //just make the transaction ID a random number, so it changes int transID = rand.Next(); //if verifyUser was called, this is a read of cached data user.readAccountData(accountData, 0); //before we update the account data array at all, let's make a backup copy Array.Copy(accountData, 0, oldAcctData, 0, 32); //get the user's balance from accountData int balance = -1; if (accountData[I_FILE_LENGTH] == RECORD_A_LENGTH) { balance = Convert.toInt(accountData, I_BALANCE_A, 3); } else if (accountData[I_FILE_LENGTH] == RECORD_B_LENGTH) { balance = Convert.toInt(accountData, I_BALANCE_B, 3); } //if there are insufficient funds if (this.debitAmount > balance) { this.lastError = USER_BAD_ACCOUNT_DATA; return(false); } //update the user's balance this.userBalance = (balance - this.debitAmount); // attempt to update the page bool success = false; try { success = writeTransactionData(user, transID, this.userBalance, accountData); } catch (System.Exception) { // sink } //if write didn't succeeded or if we need to perform //a verification step anyways, let's double-check what //the user has on the button. if (verifySuccess || !success) { OneWireEventSource.Log.Debug("attempting to re-write transaction data: "); OneWireEventSource.Log.Debug("cur Data: " + Convert.toHexString(accountData)); OneWireEventSource.Log.Debug("old data: " + Convert.toHexString(oldAcctData)); bool dataOK = false; int cnt = MAX_RETRY_CNT; do { try { // let's refresh the page user.refreshDevice(); //calling verify user re-issues a challenge-response //and reloads the cached account data in the user object. if (verifyUser(user)) { //compare the user's account data against the working //copy and the backup copy. if (user.readAccountData(newAcctData, 0)) { OneWireEventSource.Log.Debug("new data: " + Convert.toHexString(newAcctData)); bool isOld = true; bool isCur = true; for (int i = 0; i < 32 && (isOld || isCur); i++) { //match the backup isOld = isOld && (newAcctData[i] == oldAcctData[i]); //match the working copy isCur = isCur && (newAcctData[i] == accountData[i]); } if (isOld) { //if it matches the backup copy, we didn't write anything //and the data is still okay, but we didn't do a debit dataOK = true; success = false; } else if (isCur) { dataOK = true; success = true; } else { int cnt2 = MAX_RETRY_CNT; do { //iBUTTON DATA IS TOTALLY HOSED //keep trying to get account data on the button try { success = writeTransactionData(user, transID, this.userBalance, accountData); } catch (OneWireIOException owioe) { if (cnt2 == 0) { throw owioe; } } catch (OneWireException owe) { if (cnt2 == 0) { throw owe; } } } while (((cnt2 -= 1) > 0) && !success); } } } } catch (OneWireIOException owioe) { if (cnt == 0) { throw owioe; } } catch (OneWireException owe) { if (cnt == 0) { throw owe; } } } while (!dataOK && ((cnt -= 1) > 0)); //TODO if (!dataOK) { //couldn't fix the data after 255 retries IOHelper.writeLine("Catastrophic Failure!"); success = false; } } return(success); } }
/// <summary> /// <P>Verifies user's account data. Account data is "verified" if and /// only if the account balance is greater than zero. No digital /// signature is checked by this transaction.</P> /// /// <P><B>Flow of action: </B> /// <ul> /// <li> Read the account data from user </li> /// <li> Extract account balance </li> /// <li> Debit money from balance </li> /// <li> Insert the new balance </li> /// <li> Write the account data to the user </li> /// </ul></P> /// /// <P>If previous steps have been executed, all "Read" commands on /// the user are reading from cached data.</P> /// </summary> /// <param name="user"> SHAiButtonUser upon which the transaction occurs. /// </param> /// <returns> <code>true</code> if and only if the account balance is /// greater than zero. /// </returns> /// <seealso cref= SHAiButtonUser#readAccountData(byte[],int) </seealso> /// <seealso cref= #getLastError() </seealso> public override bool verifyTransactionData(SHAiButtonUser user) { lock (this) { //clear any error this.lastError = NO_ERROR; byte[] accountData = this.verifyData_accountData; //if verifyUser was called, this is a read of cached data user.readAccountData(accountData, 0); // verify the A-B data scheme is valid bool validPtr = false, validA = false, validB = false; byte fileLength = accountData[I_FILE_LENGTH]; int crc16 = CRC16.compute(accountData, 0, fileLength + 3, user.AccountPageNumber); if (fileLength == RECORD_A_LENGTH || fileLength == RECORD_B_LENGTH) { validPtr = true; } // was the crc of the file correct? if (crc16 == 0xB001) { // if the header points to a valid record, we're done if (validPtr) { // nothing more to check for DS1961S/DS2432, since // it carries no signed data, we're finished return(true); } // header points to neither record A nor B, but // crc is absolutely correct. that can only mean // we're looking at something that is the wrong // size from what was expected, but apparently is // exactly what was meant to be written. I'm done. this.lastError = USER_BAD_ACCOUNT_DATA; return(false); } // restore the other header information accountData[I_DATA_TYPE_CODE] = 0x01; accountData[I_CONVERSION_FACTOR + 0] = 0x8B; accountData[I_CONVERSION_FACTOR + 1] = 0x48; // zero-out the don't care bytes accountData[I_DONT_CARE] = 0x00; accountData[I_DONT_CARE + 1] = 0x00; accountData[I_DONT_CARE + 2] = 0x00; accountData[I_DONT_CARE + 3] = 0x00; // lets try Record A and check the crc accountData[I_FILE_LENGTH] = RECORD_A_LENGTH; crc16 = CRC16.compute(accountData, 0, RECORD_A_LENGTH + 3, user.AccountPageNumber); if (crc16 == 0xB001) { validA = true; } // lets try Record B and check the crc accountData[I_FILE_LENGTH] = RECORD_B_LENGTH; crc16 = CRC16.compute(accountData, 0, RECORD_B_LENGTH + 3, user.AccountPageNumber); if (crc16 == 0xB001) { validB = true; } if (validA && validB) { OneWireEventSource.Log.Debug("Both A and B are valid"); // Both A & B are valid! And we know that we can only // get here if the pointer or the header was not valid. // That means that B was the last updated one but the // header got hosed and the debit was not finished... // which means A is the last known good value, let's go with A. accountData[I_FILE_LENGTH] = RECORD_A_LENGTH; } else if (validA) { OneWireEventSource.Log.Debug("A is valid not B"); // B is invalid, A is valid. Means A is the last updated one, // but B is the last known good value. The header was not updated // to point to A before debit was aborted. Let's go with B accountData[I_FILE_LENGTH] = RECORD_B_LENGTH; } else if (validB) { OneWireEventSource.Log.Debug("B is valid not A - impossible"); // A is invalid, B is valid. Should never ever happen. Something // got completely hosed. What should happen here? this.lastError = USER_BAD_ACCOUNT_DATA; return(false); } else { OneWireEventSource.Log.Debug("Neither record has valid CRC"); // neither record contains a valid CRC. What should happen here? // probably got weak bit in ptr record, no telling which way to go this.lastError = USER_BAD_ACCOUNT_DATA; return(false); } //get the user's balance from accountData int balance = -1; if (accountData[I_FILE_LENGTH] == RECORD_A_LENGTH) { balance = Convert.toInt(accountData, I_BALANCE_A, 3); } else if (accountData[I_FILE_LENGTH] == RECORD_B_LENGTH) { balance = Convert.toInt(accountData, I_BALANCE_B, 3); } if (balance < 0) { this.lastError = USER_BAD_ACCOUNT_DATA; return(false); } this.userBalance = balance; int cnt = MAX_RETRY_CNT; do { if (user.refreshDevice() && writeTransactionData(user, -1, balance, accountData)) { break; } } while (--cnt > 0); return(false); } }