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
- NFC Reader → Sends SELECT AID command
- Android → Routes to HCE service with matching AID
- HCE Service → Processes APDU commands
- 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) {