HCE (Host Card Emulation) in Java: Complete Implementation Guide

HCE allows Android devices to emulate NFC cards without relying on a secure element. Here's a comprehensive guide to implementing HCE in Java for Android applications.


HCE Architecture Overview

Key Components

  • Host-based Card Emulation: NFC card emulation handled by app
  • AID (Application ID): Unique identifier for NFC applications
  • APDU (Application Protocol Data Unit): Command-response protocol
  • ISO 7816-4: Standard for smart card communication

HCE Flow

  1. NFC Reader → Sends SELECT AID command
  2. Android → Routes to HCE service with matching AID
  3. HCE Service → Processes APDU commands
  4. HCE Service → Returns APDU responses

Basic HCE Service Implementation

Example 1: Basic HCE Service for Loyalty Cards

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.hce">
<!-- NFC Permission -->
<uses-permission android:name="android.permission.NFC" />
<!-- NFC Feature -->
<uses-feature
android:name="android.hardware.nfc"
android:required="true" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<!-- Main Activity -->
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- HCE Service -->
<service
android:name=".LoyaltyCardService"
android:exported="true"
android:permission="android.permission.BIND_NFC_SERVICE">
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
</intent-filter>
<meta-data
android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/apduservice" />
</service>
</application>
</manifest>

apduservice.xml

<?xml version="1.0" encoding="utf-8"?>
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/service_description"
android:requireDeviceUnlock="false">
<!-- Loyalty Card AID -->
<aid-group 
android:description="@string/aid_group_description"
android:category="other">
<aid-filter android:name="F0010203040506" />
<aid-filter android:name="F0394148148100" />
</aid-group>
</host-apdu-service>

LoyaltyCardService.java

package com.example.hce;
import android.nfc.cardemulation.HostApduService;
import android.os.Bundle;
import android.util.Log;
import java.util.Arrays;
public class LoyaltyCardService extends HostApduService {
private static final String TAG = "LoyaltyCardService";
// APDU commands
private static final String SELECT_APDU_HEADER = "00A40400";
private static final byte[] SELECT_OK_SW = HexStringToByteArray("9000");
private static final byte[] UNKNOWN_CMD_SW = HexStringToByteArray("0000");
private static final byte[] SELECT_APDU = BuildSelectApdu("F0010203040506");
// Loyalty card data
private LoyaltyCard currentCard;
private int transactionCounter = 0;
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "HCE Service created");
// Initialize with sample loyalty card
currentCard = new LoyaltyCard(
"1234567890",
"John Doe",
1500,  // Points
"GOLD",
System.currentTimeMillis()
);
}
@Override
public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
Log.d(TAG, "Received APDU: " + ByteArrayToHexString(commandApdu));
// Check for SELECT AID command
if (Arrays.equals(SELECT_APDU, commandApdu)) {
Log.i(TAG, "Application selected");
return ConcatArrays(currentCard.toApdu(), SELECT_OK_SW);
}
// Parse the command
String command = ByteArrayToHexString(commandApdu);
if (command.startsWith("80")) { // Proprietary commands
return processProprietaryCommand(commandApdu);
}
if (command.startsWith("00")) { // ISO 7816-4 commands
return processISOCommand(commandApdu);
}
Log.w(TAG, "Unknown command: " + command);
return UNKNOWN_CMD_SW;
}
private byte[] processProprietaryCommand(byte[] commandApdu) {
byte cla = commandApdu[0];
byte ins = commandApdu[1];
byte p1 = commandApdu[2];
byte p2 = commandApdu[3];
switch (ins) {
case (byte) 0x10: // GET_LOYALTY_DATA
return processGetLoyaltyData();
case (byte) 0x20: // ADD_POINTS
return processAddPoints(commandApdu);
case (byte) 0x30: // REDEEM_POINTS
return processRedeemPoints(commandApdu);
case (byte) 0x40: // GET_TRANSACTION_HISTORY
return processGetTransactionHistory(p1);
default:
return UNKNOWN_CMD_SW;
}
}
private byte[] processISOCommand(byte[] commandApdu) {
byte ins = commandApdu[1];
switch (ins) {
case (byte) 0xB0: // READ BINARY
return processReadBinary(commandApdu);
case (byte) 0xD6: // UPDATE BINARY
return processUpdateBinary(commandApdu);
case (byte) 0x84: // GET CHALLENGE
return processGetChallenge();
default:
return UNKNOWN_CMD_SW;
}
}
private byte[] processGetLoyaltyData() {
Log.i(TAG, "Processing GET_LOYALTY_DATA command");
try {
byte[] response = currentCard.toApdu();
return ConcatArrays(response, SELECT_OK_SW);
} catch (Exception e) {
Log.e(TAG, "Error processing GET_LOYALTY_DATA", e);
return HexStringToByteArray("6F00"); // Internal error
}
}
private byte[] processAddPoints(byte[] commandApdu) {
if (commandApdu.length < 7) {
return HexStringToByteArray("6700"); // Wrong length
}
// Extract points from command data
int pointsToAdd = ((commandApdu[5] & 0xFF) << 8) | (commandApdu[6] & 0xFF);
Log.i(TAG, "Adding " + pointsToAdd + " points to card");
// Update card points
currentCard.addPoints(pointsToAdd);
// Log transaction
transactionCounter++;
// Return updated points balance
byte[] pointsData = new byte[] {
(byte) ((currentCard.getPoints() >> 8) & 0xFF),
(byte) (currentCard.getPoints() & 0xFF)
};
return ConcatArrays(pointsData, SELECT_OK_SW);
}
private byte[] processRedeemPoints(byte[] commandApdu) {
if (commandApdu.length < 7) {
return HexStringToByteArray("6700"); // Wrong length
}
int pointsToRedeem = ((commandApdu[5] & 0xFF) << 8) | (commandApdu[6] & 0xFF);
if (pointsToRedeem > currentCard.getPoints()) {
return HexStringToByteArray("6A85"); // Not enough points
}
Log.i(TAG, "Redeeming " + pointsToRedeem + " points");
// Update card points
currentCard.redeemPoints(pointsToRedeem);
// Log transaction
transactionCounter++;
// Return success with remaining points
byte[] pointsData = new byte[] {
(byte) ((currentCard.getPoints() >> 8) & 0xFF),
(byte) (currentCard.getPoints() & 0xFF)
};
return ConcatArrays(pointsData, SELECT_OK_SW);
}
private byte[] processGetTransactionHistory(byte page) {
// Simulate transaction history
byte[] history = new byte[16];
for (int i = 0; i < history.length; i++) {
history[i] = (byte) (transactionCounter + i);
}
return ConcatArrays(history, SELECT_OK_SW);
}
private byte[] processReadBinary(byte[] commandApdu) {
int offset = ((commandApdu[2] & 0xFF) << 8) | (commandApdu[3] & 0xFF);
int length = commandApdu[4] & 0xFF;
Log.d(TAG, "READ BINARY: offset=" + offset + ", length=" + length);
// Return card data based on offset
byte[] data = currentCard.readBinary(offset, length);
if (data != null) {
return ConcatArrays(data, SELECT_OK_SW);
} else {
return HexStringToByteArray("6A82"); // File not found
}
}
private byte[] processUpdateBinary(byte[] commandApdu) {
int offset = ((commandApdu[2] & 0xFF) << 8) | (commandApdu[3] & 0xFF);
if (commandApdu.length < 6) {
return HexStringToByteArray("6700"); // Wrong length
}
byte[] data = Arrays.copyOfRange(commandApdu, 5, commandApdu.length - 1);
Log.d(TAG, "UPDATE BINARY: offset=" + offset + ", data=" + ByteArrayToHexString(data));
boolean success = currentCard.updateBinary(offset, data);
return success ? SELECT_OK_SW : HexStringToByteArray("6A84"); // Insufficient memory
}
private byte[] processGetChallenge() {
// Generate random challenge for security
byte[] challenge = new byte[8];
new java.security.SecureRandom().nextBytes(challenge);
return ConcatArrays(challenge, SELECT_OK_SW);
}
@Override
public void onDeactivated(int reason) {
Log.d(TAG, "HCE Service deactivated. Reason: " + reason);
switch (reason) {
case DEACTIVATION_LINK_LOSS:
Log.i(TAG, "NFC link lost");
break;
case DEACTIVATION_DESELECTED:
Log.i(TAG, "Application deselected");
break;
}
}
// Utility methods
public static String ByteArrayToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02X", b));
}
return sb.toString();
}
public static byte[] HexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
public static byte[] ConcatArrays(byte[] first, byte[] second) {
byte[] result = Arrays.copyOf(first, first.length + second.length);
System.arraycopy(second, 0, result, first.length, second.length);
return result;
}
public static byte[] BuildSelectApdu(String aid) {
return HexStringToByteArray(SELECT_APDU_HEADER + String.format("%02X", aid.length() / 2) + aid);
}
}
// Loyalty Card Data Model
class LoyaltyCard {
private String cardNumber;
private String holderName;
private int points;
private String tier;
private long lastUpdated;
private byte[] cardData;
public LoyaltyCard(String cardNumber, String holderName, int points, String tier, long lastUpdated) {
this.cardNumber = cardNumber;
this.holderName = holderName;
this.points = points;
this.tier = tier;
this.lastUpdated = lastUpdated;
updateCardData();
}
private void updateCardData() {
// Convert card data to byte array for APDU response
try {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
java.io.DataOutputStream dos = new java.io.DataOutputStream(baos);
// Card structure:
// 0-9: Card number (10 bytes)
// 10-29: Holder name (20 bytes)
// 30-31: Points (2 bytes)
// 32: Tier (1 byte)
// 33-40: Last updated (8 bytes - timestamp)
// Write card number (padded to 10 bytes)
byte[] cardNumberBytes = Arrays.copyOf(cardNumber.getBytes("UTF-8"), 10);
dos.write(cardNumberBytes);
// Write holder name (padded to 20 bytes)
byte[] nameBytes = Arrays.copyOf(holderName.getBytes("UTF-8"), 20);
dos.write(nameBytes);
// Write points
dos.writeShort(points);
// Write tier
byte tierByte = 0;
switch (tier) {
case "BRONZE": tierByte = 1; break;
case "SILVER": tierByte = 2; break;
case "GOLD": tierByte = 3; break;
case "PLATINUM": tierByte = 4; break;
}
dos.writeByte(tierByte);
// Write timestamp
dos.writeLong(lastUpdated);
dos.flush();
cardData = baos.toByteArray();
dos.close();
} catch (Exception e) {
Log.e("LoyaltyCard", "Error updating card data", e);
cardData = new byte[41]; // Default empty data
}
}
public byte[] toApdu() {
return cardData;
}
public byte[] readBinary(int offset, int length) {
if (offset < 0 || offset >= cardData.length) {
return null;
}
int end = Math.min(offset + length, cardData.length);
return Arrays.copyOfRange(cardData, offset, end);
}
public boolean updateBinary(int offset, byte[] data) {
if (offset < 0 || offset + data.length > cardData.length) {
return false;
}
System.arraycopy(data, 0, cardData, offset, data.length);
// Update fields from modified data
try {
java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream(cardData);
java.io.DataInputStream dis = new java.io.DataInputStream(bais);
// Read card number
byte[] cardNumberBytes = new byte[10];
dis.readFully(cardNumberBytes);
cardNumber = new String(cardNumberBytes, "UTF-8").trim();
// Read holder name
byte[] nameBytes = new byte[20];
dis.readFully(nameBytes);
holderName = new String(nameBytes, "UTF-8").trim();
// Read points
points = dis.readUnsignedShort();
// Read tier
byte tierByte = dis.readByte();
switch (tierByte) {
case 1: tier = "BRONZE"; break;
case 2: tier = "SILVER"; break;
case 3: tier = "GOLD"; break;
case 4: tier = "PLATINUM"; break;
default: tier = "UNKNOWN";
}
// Read timestamp
lastUpdated = dis.readLong();
dis.close();
} catch (Exception e) {
Log.e("LoyaltyCard", "Error parsing updated card data", e);
return false;
}
return true;
}
// Getters and setters
public String getCardNumber() { return cardNumber; }
public String getHolderName() { return holderName; }
public int getPoints() { return points; }
public String getTier() { return tier; }
public long getLastUpdated() { return lastUpdated; }
public void addPoints(int pointsToAdd) {
this.points += pointsToAdd;
this.lastUpdated = System.currentTimeMillis();
updateCardData();
}
public void redeemPoints(int pointsToRedeem) {
this.points -= pointsToRedeem;
this.lastUpdated = System.currentTimeMillis();
updateCardData();
}
}

Advanced HCE with Security

Example 2: Secure Payment Card Emulation

SecurePaymentService.java

```java
package com.example.hce;

import android.nfc.cardemulation.HostApduService;
import android.os.Bundle;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Log;
import javax.crypto.; import javax.crypto.spec.GCMParameterSpec; import java.security.;
import java.util.Arrays;

public class SecurePaymentService extends HostApduService {
private static final String TAG = "SecurePaymentService";

// AID for payment application
private static final String PAYMENT_AID = "A0000000041010";
private static final byte[] SELECT_OK_SW = HexStringToByteArray("9000");
private static final byte[] UNKNOWN_CMD_SW = HexStringToByteArray("0000");
// Crypto constants
private static final String KEY_ALIAS = "HCE_PAYMENT_KEY";
private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
private static final int GCM_TAG_LENGTH = 128;
// Payment card data
private PaymentCard paymentCard;
private KeyStore keyStore;
private Cipher cipher;
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "Secure Payment HCE Service created");
initializeCryptography();
initializePaymentCard();
}
private void initializeCryptography() {
try {
// Initialize Android KeyStore
keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null);
// Generate or retrieve encryption key
if (!keyStore.containsAlias(KEY_ALIAS)) {
generateKey();
}
// Initialize cipher
cipher = Cipher.getInstance("AES/GCM/NoPadding");
} catch (Exception e) {
Log.e(TAG, "Error initializing cryptography", e);
throw new RuntimeException("Cryptography initialization failed", e);
}
}
private void generateKey() throws Exception {
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.setUserAuthenticationRequired(false);
keyGenerator.init(builder.build());
keyGenerator.generateKey();
Log.i(TAG, "Generated new encryption key");
}
private void initializePaymentCard() {
// Initialize with secure payment card data
paymentCard = new PaymentCard(
"************1234", // Masked PAN
"John Doe",
"12/25", // Expiry
"123",   // CVV
generateDynamicCVV() // Dynamic CVV for HCE
);
}
private String generateDynamicCVV() {
// Generate dynamic CVV based on time and counter
// This is a simplified example
long time = System.currentTimeMillis() / (30 * 1000); // 30-second window
int counter = paymentCard.getTransactionCounter();
String base = String.valueOf(time + counter);
return base.substring(base.length() - 3); // Last 3 digits
}
@Override
public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
Log.d(TAG, "Processing APDU: " + ByteArrayToHexString(commandApdu));
String command = ByteArrayToHexString(commandApdu);
try {
if (isSelectCommand(commandApdu)) {
return processSelectCommand();
}
if (command.startsWith("80")) { // Payment application commands
return processPaymentCommand(commandApdu);
}
if (command.startsWith("00")) { // ISO commands
return processISOCommand(commandApdu);
}
Log.w(TAG, "Unknown command: " + command);
return UNKNOWN_CMD_SW;
} catch (SecurityException e) {
Log.e(TAG, "Security error processing command", e);
return HexStringToByteArray("6982"); // Security status not satisfied
} catch (Exception e) {
Log.e(TAG, "Error processing command", e);
return HexStringToByteArray("6F00"); // Internal error
}
}
private boolean isSelectCommand(byte[] commandApdu) {
if (commandApdu.length < 5) return false;
byte[] selectHeader = Arrays.copyOf(commandApdu, 4);
byte[] expectedHeader = HexStringToByteArray("00A40400");
return Arrays.equals(selectHeader, expectedHeader);
}
private byte[] processSelectCommand() {
Log.i(TAG, "Payment application selected");
try {
// Return payment application FCI (File Control Information)
byte[] fci = buildFCI();
return ConcatArrays(fci, SELECT_OK_SW);
} catch (Exception e) {
Log.e(TAG, "Error building FCI", e);
return HexStringToByteArray("6F00");
}
}
private byte[] buildFCI() {
// Build File Control Information for payment application
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
// FCI template
baos.write(0x6F); // FCI template tag
// Payment system specific template
baos.write(0xA5); // Directory descriptor
// DF Name (AID)
baos.write(0x84); // DF Name tag
baos.write(PAYMENT_AID.length() / 2);
baos.write(HexStringToByteArray(PAYMENT_AID));
// FCI proprietary template
baos.write(0xA5); // Another template
baos.write(0x50); // Application label
baos.write("MyPaymentApp".getBytes("UTF-8").length);
baos.write("MyPaymentApp".getBytes("UTF-8"));
} catch (Exception e) {
Log.e(TAG, "Error building FCI", e);
}
return baos.toByteArray();
}
private byte[] processPaymentCommand(byte[] commandApdu) {
byte ins = commandApdu[1];
switch (ins) {
case (byte) 0x10: // GET_PAYMENT_DATA
return processGetPaymentData();
case (byte) 0x20: // PROCESS_PAYMENT
return processPayment(commandApdu);
case (byte) 0x30: // GENERATE_CRYPTOGRAM
return generateCryptogram(commandApdu);
case (byte) 0x40: // VERIFY_PIN
return verifyPin(commandApdu);
default:
return HexStringToByteArray("6A86"); // Incorrect parameters
}
}
private byte[] processGetPaymentData() {
Log.i(TAG, "Processing GET_PAYMENT_DATA");
try {
// Return encrypted payment data
byte[] paymentData = paymentCard.toEncryptedApdu(getEncryptionKey());
return ConcatArrays(paymentData, SELECT_OK_SW);
} catch (Exception e) {
Log.e(TAG, "Error encrypting payment data", e);
return HexStringToByteArray("6F00");
}
}
private byte[] processPayment(byte[] commandApdu) {
if (commandApdu.length < 8) {
return HexStringToByteArray("6700"); // Wrong length
}
try {
// Extract transaction amount
int amount = ((commandApdu[5] & 0xFF) << 16) | 
((commandApdu[6] & 0xFF) << 8) | 
(commandApdu[7] & 0xFF);
Log.i(TAG, "Processing payment: " + amount);
// Validate transaction
if (!paymentCard.validateTransaction(amount)) {
return HexStringToByteArray("6985"); // Conditions not satisfied
}
// Generate transaction cryptogram
byte[] cryptogram = generateTransactionCryptogram(amount);
// Update transaction counter
paymentCard.incrementTransactionCounter();
return ConcatArrays(cryptogram, SELECT_OK_SW);
} catch (Exception e) {
Log.e(TAG, "Error processing payment", e);
return HexStringToByteArray("6F00");
}
}
private byte[] generateCryptogram(byte[] commandApdu) {
// Generate application cryptogram for transaction
try {
byte[] data = Arrays.copyOfRange(commandApdu, 5, commandApdu.length - 1);
byte[] cryptogram = generateARQC(data); // Authorization Request Cryptogram
return ConcatArrays(cryptogram, SELECT_OK_SW);
} catch (Exception e) {
Log.e(TAG, "Error generating cryptogram", e);
return HexStringToByteArray("6F00");
}
}
private byte[] verifyPin(byte[] commandApdu) {
if (commandApdu.length < 7) {
return HexStringToByteArray("6700"); // Wrong length
}
byte pinLength = commandApdu[5];
if (commandApdu.length < 6 + pinLength) {
return HexStringToByteArray("6700");
}
byte[] pinAttempt = Arrays.copyOfRange(commandApdu, 6, 6 + pinLength);
// Verify PIN (in real implementation, use secure comparison)
boolean pinVerified = paymentCard.verifyPin(pinAttempt);
if (pinVerified) {
paymentCard.setPinVerified(true);
return SELECT_OK_SW;
} else {
paymentCard.incrementPinAttempts();
return HexStringToByteArray("63C0"); // PIN verification failed
}
}
private byte[] processISOCommand(byte[] commandApdu) {
byte ins = commandApdu[1];
switch (ins) {
case (byte) 0x84: // GET_CHALLENGE
return getChallenge();
case (byte) 0x88: // INTERNAL_AUTHENTICATE
return internalAuthenticate(commandApdu);
case (byte) 0xB2: // READ_RECORD
return readRecord(commandApdu);
default:
return UNKNOWN_CMD_SW;
}
}
private byte[] getChallenge() {
// Generate random challenge
byte[] challenge = new byte[8];
new SecureRandom().nextBytes(challenge);
paymentCard.setLastChallenge(challenge);
return ConcatArrays(challenge, SELECT_OK_SW);
}
private byte[] internalAuthenticate(byte[] commandApdu) {
if (commandApdu.length < 7) {
return HexStringToByteArray("6700");
}
byte dataLength = commandApdu[5];
if (commandApdu.length < 6 + dataLength) {
return HexStringToByteArray("6700");
}
byte[] authenticationData = Arrays.copyOfRange(commandApdu, 6, 6 + dataLength);
try {
// Generate authentication response
byte[] response = generateAuthenticationResponse(authenticationData);
return ConcatArrays(response, SELECT_OK_SW);
} catch (Exception e) {
Log.e(TAG, "Error in internal authenticate", e);
return HexStringToByteArray("6F00");
}
}
private byte[] readRecord(byte[] commandApdu) {
byte p1 = commandApdu[2];
byte p2 = commandApdu[3];
// Read specific record based on P1 and P2
byte[] recordData = paymentCard.readRecord(p1, p2);
if (recordData != null) {
return ConcatArrays(recordData, SELECT_OK_SW);
} else {
return HexStringToByteArray("6A83"); // Record not found
}
}
private SecretKey getEncryptionKey() throws Exception {
KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry(KEY_ALIAS, null);
return secretKeyEntry.getSecretKey();
}
private byte[] generateARQC(byte[] data) throws Exception {
// Generate Authorization Request Cryptogram
// This is a simplified version
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data);
return Arrays.copyOf(hash, 8); // Return first 8 bytes as ARQC
}
private byte[] generateTransactionCryptogram(int amount) throws Exception {
// Generate transaction-specific cryptogram
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(paymentCard.getTransactionCounter());
baos.write((byte) ((amount >> 16) & 0xFF));
baos.write((byte) ((amount >> 8) & 0xFF));
baos.write((byte) (amount & 0xFF));
baos.write(System.currentTimeMillis());
return generateARQC(baos.toByteArray());
}
private byte[] generateAuthenticationResponse(byte[] data) throws Exception {
// Generate response to authentication challenge
SecretKey key = getEncryptionKey();
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] iv = new byte[12]; // GCM IV
new SecureRandom().nextBytes(iv);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] encrypted = cipher.doFinal(data);
// Combine IV and encrypted data
ByteArrayOutputStream response = new ByteArrayOutputStream();
response.write(iv);
response.write(encrypted);
return response.toByteArray();
}
@Override
public void onDeactivated(int reason) {
Log.d(TAG, "Payment HCE Service deactivated. Reason: " + reason);
// Reset sensitive state
paymentCard.setPinVerified(false);
}
// Utility methods (same as previous example)
public static String ByteArrayToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02X", b));
}
return sb.toString();
}
public static byte[] HexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
public static byte[] ConcatArrays(byte[] first, byte[] second) {
byte[] result = Arrays.copyOf(first, first.length + second.length);
System.arraycopy(second, 0, result, first.length, second.length);
return result;
}

}

// Secure Payment Card Data Model
class PaymentCard {
private String maskedPan;
private String cardholderName;
private String expiryDate;
private String staticCvv;
private String dynamicCvv;
private int transactionCounter;
private boolean pinVerified;
private int pinAttempts;
private byte[] lastChallenge;

public PaymentCard(String maskedPan, String cardholderName, String expiryDate, 
String staticCvv, String dynamicCvv) {
this.maskedPan = maskedPan;
this.cardholderName = cardholderName;
this.expiryDate = expiryDate;
this.staticCvv = staticCvv;
this.dynamicCvv = dynamicCvv;
this.transactionCounter = 0;
this.pinVerified = false;
this.pinAttempts = 0;
}
public byte[] toEncryptedApdu(SecretKey key) throws Exception {
// Convert card data to encrypted APDU response
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
// Write card data
dos.write(maskedPan.getBytes("UTF-8"));
dos.write(cardholderName.getBytes("UTF-8"));
dos.write(expiryDate.getBytes("UTF-8"));
dos.write(dynamicCvv.getBytes("UTF-8"));
dos.writeInt(transactionCounter);
dos.flush();
byte[] plaintext = baos.toByteArray();
dos.close();
// Encrypt the data
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
byte[] iv = new byte[12]; // GCM IV
new SecureRandom().nextBytes(iv);
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] encrypted = cipher.doFinal(plaintext);
// Combine IV and encrypted data
ByteArrayOutputStream result = new ByteArrayOutputStream();
result.write(iv);
result.write(encrypted);
return result.toByteArray();
}
public boolean validateTransaction(int amount) {
// Validate transaction amount and conditions
if (amount <= 0) return false;
if (amount > 1000000) return false; // Example limit: 10,000.00
// Check if PIN verification is required for large amounts
if (amount > 50000 && !pinVerified) {
return false;
}
return true;
}
public boolean verifyPin(byte[] pinAttempt) {
// In real implementation, use secure PIN verification
// This is a simplified example
String attempt = new String(pinAttempt).trim();
// Compare with stored PIN (in real app, use secure storage)
String storedPin = "1234"; // This should be securely stored
boolean verified = storedPin.equals(attempt);
if (verified) {
pinAttempts = 0;
}
return verified;
}
public byte[] readRecord(byte p1, byte p2) {
// Return specific record based on P1 and P2
// This simulates EMV card file structure
int recordNumber = p2 & 0x0F;
int sfi = (p1 << 4) | (p2 >> 4); // Short File Identifier
switch (sfi) {

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper