NFC (Near Field Communication) enables short-range wireless communication between devices. Here's a comprehensive guide to implementing NFC reading and writing capabilities in Java.
NFC Architecture Overview
Key NFC Technologies
- NFC-A/B/F: Different communication protocols
- NDEF (NFC Data Exchange Format): Standard data format
- MIME Types: Data type identification
- NFC Tags: Types 1-5 with different capabilities
Java NFC APIs
- javax.smartcardio: Basic smart card communication
- JSR 257 (Obsolete): Former NFC API
- Third-party Libraries: ACS, Spring NFC, etc.
- Android NFC: Android-specific API
Basic Smart Card IO Implementation
Example 1: Basic NFC Reader using javax.smartcardio
import javax.smartcardio.*;
import java.util.List;
public class BasicNFCReader {
private TerminalFactory terminalFactory;
private List<CardTerminal> terminals;
private CardTerminal terminal;
private Card card;
private CardChannel channel;
public BasicNFCReader() {
initializeReader();
}
private void initializeReader() {
try {
// Get terminal factory
terminalFactory = TerminalFactory.getDefault();
terminals = terminalFactory.terminals().list();
if (terminals.isEmpty()) {
System.out.println("No NFC readers found");
return;
}
// Use first available terminal
terminal = terminals.get(0);
System.out.println("Using reader: " + terminal.getName());
} catch (CardException e) {
System.err.println("Error initializing NFC reader: " + e.getMessage());
}
}
public boolean waitForCard() {
if (terminal == null) {
System.err.println("No terminal available");
return false;
}
try {
System.out.println("Waiting for NFC card...");
terminal.waitForCardPresent(0); // Wait indefinitely
return true;
} catch (CardException e) {
System.err.println("Error waiting for card: " + e.getMessage());
return false;
}
}
public boolean connectToCard() {
try {
if (terminal.isCardPresent()) {
card = terminal.connect("T=1"); // T=0 for contact, T=1 for contactless
channel = card.getBasicChannel();
System.out.println("Connected to card: " + card);
return true;
}
} catch (CardException e) {
System.err.println("Error connecting to card: " + e.getMessage());
}
return false;
}
public byte[] sendAPDU(byte[] command) {
if (channel == null) {
System.err.println("Not connected to card");
return null;
}
try {
CommandAPDU apdu = new CommandAPDU(command);
ResponseAPDU response = channel.transmit(apdu);
return response.getBytes();
} catch (CardException e) {
System.err.println("Error sending APDU: " + e.getMessage());
return null;
}
}
public void disconnect() {
try {
if (card != null) {
card.disconnect(false);
card = null;
channel = null;
System.out.println("Disconnected from card");
}
} catch (CardException e) {
System.err.println("Error disconnecting: " + e.getMessage());
}
}
public void waitForCardRemoval() {
try {
if (terminal != null) {
terminal.waitForCardAbsent(0);
System.out.println("Card removed");
}
} catch (CardException e) {
System.err.println("Error waiting for card removal: " + e.getMessage());
}
}
// Common NFC commands
public byte[] selectApplication(byte[] aid) {
// SELECT command APDU
byte[] selectCommand = new byte[5 + aid.length];
selectCommand[0] = (byte) 0x00; // CLA
selectCommand[1] = (byte) 0xA4; // INS: SELECT
selectCommand[2] = (byte) 0x04; // P1: Select by name
selectCommand[3] = (byte) 0x00; // P2: First or only occurrence
selectCommand[4] = (byte) aid.length; // Lc: Length of AID
System.arraycopy(aid, 0, selectCommand, 5, aid.length);
return sendAPDU(selectCommand);
}
public byte[] readBinary(int offset, int length) {
byte[] readCommand = new byte[5];
readCommand[0] = (byte) 0x00; // CLA
readCommand[1] = (byte) 0xB0; // INS: READ BINARY
readCommand[2] = (byte) ((offset >> 8) & 0xFF); // P1: High byte of offset
readCommand[3] = (byte) (offset & 0xFF); // P2: Low byte of offset
readCommand[4] = (byte) length; // Le: Expected length
return sendAPDU(readCommand);
}
public static void main(String[] args) {
BasicNFCReader reader = new BasicNFCReader();
try {
while (true) {
if (reader.waitForCard()) {
if (reader.connectToCard()) {
// Try to read card UID (varies by card type)
byte[] response = reader.selectApplication(new byte[]{
(byte) 0xF0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00
});
if (response != null) {
System.out.println("Card response: " + bytesToHex(response));
}
reader.disconnect();
reader.waitForCardRemoval();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X ", b));
}
return sb.toString();
}
}
Advanced NFC Framework
Example 2: Comprehensive NFC Framework
import javax.smartcardio.*;
import java.util.*;
import java.util.concurrent.*;
public class AdvancedNFCManager {
private final List<CardTerminal> terminals;
private final Map<String, NFCCardHandler> cardHandlers;
private final ScheduledExecutorService scheduler;
private final List<NFCEventListener> listeners;
private boolean monitoring;
public AdvancedNFCManager() {
this.terminals = new ArrayList<>();
this.cardHandlers = new HashMap<>();
this.scheduler = Executors.newScheduledThreadPool(2);
this.listeners = new ArrayList<>();
this.monitoring = false;
initializeHandlers();
discoverTerminals();
}
private void initializeHandlers() {
// Register handlers for different card types
cardHandlers.put("MIFARE_CLASSIC", new MifareClassicHandler());
cardHandlers.put("MIFARE_ULTRALIGHT", new MifareUltralightHandler());
cardHandlers.put("ISO14443A", new ISO14443AHandler());
cardHandlers.put("NDEF", new NDEFHandler());
cardHandlers.put("UNKNOWN", new UnknownCardHandler());
}
private void discoverTerminals() {
try {
TerminalFactory factory = TerminalFactory.getDefault();
List<CardTerminal> discovered = factory.terminals().list();
terminals.clear();
terminals.addAll(discovered);
System.out.println("Discovered " + terminals.size() + " NFC reader(s):");
for (CardTerminal terminal : terminals) {
System.out.println(" - " + terminal.getName());
}
} catch (CardException e) {
System.err.println("Error discovering terminals: " + e.getMessage());
}
}
public void startMonitoring() {
if (monitoring) {
System.out.println("Already monitoring");
return;
}
monitoring = true;
System.out.println("Starting NFC monitoring...");
// Schedule terminal discovery
scheduler.scheduleAtFixedRate(this::discoverTerminals, 0, 30, TimeUnit.SECONDS);
// Start monitoring each terminal
for (CardTerminal terminal : terminals) {
startTerminalMonitoring(terminal);
}
}
public void stopMonitoring() {
monitoring = false;
scheduler.shutdown();
System.out.println("NFC monitoring stopped");
}
private void startTerminalMonitoring(CardTerminal terminal) {
scheduler.execute(() -> {
while (monitoring) {
try {
if (terminal.waitForCardPresent(1000)) {
handleCardPresent(terminal);
}
if (terminal.waitForCardAbsent(1000)) {
handleCardRemoved(terminal);
}
} catch (CardException e) {
if (monitoring) {
System.err.println("Error monitoring terminal " + terminal.getName() + ": " + e.getMessage());
}
}
}
});
}
private void handleCardPresent(CardTerminal terminal) {
try {
Card card = terminal.connect("T=1");
NFCCardInfo cardInfo = identifyCard(card);
System.out.println("Card detected: " + cardInfo);
// Notify listeners
for (NFCEventListener listener : listeners) {
listener.onCardDetected(cardInfo);
}
// Handle card based on type
NFCCardHandler handler = cardHandlers.get(cardInfo.getType());
if (handler != null) {
handler.handleCard(card, cardInfo);
} else {
cardHandlers.get("UNKNOWN").handleCard(card, cardInfo);
}
card.disconnect(false);
} catch (CardException e) {
System.err.println("Error handling card on " + terminal.getName() + ": " + e.getMessage());
}
}
private void handleCardRemoved(CardTerminal terminal) {
System.out.println("Card removed from " + terminal.getName());
for (NFCEventListener listener : listeners) {
listener.onCardRemoved(terminal.getName());
}
}
private NFCCardInfo identifyCard(Card card) {
NFCCardInfo info = new NFCCardInfo();
info.setTerminalName(card.getTerminal().getName());
info.setProtocol(card.getProtocol());
try {
// Try different identification methods
CardChannel channel = card.getBasicChannel();
// Send SELECT command to identify card type
byte[] selectResponse = sendAPDU(channel, new byte[]{
(byte) 0x00, (byte) 0xA4, (byte) 0x04, (byte) 0x00, (byte) 0x00
});
if (selectResponse != null) {
analyzeATR(card.getATR().getBytes(), info);
analyzeSelectResponse(selectResponse, info);
}
} catch (Exception e) {
System.err.println("Error identifying card: " + e.getMessage());
}
return info;
}
private void analyzeATR(byte[] atr, NFCCardInfo info) {
// Analyze Answer To Reset to identify card type
if (atr.length > 0) {
info.setAtrHex(bytesToHex(atr));
// Simple ATR analysis (simplified)
if (atr.length >= 2) {
if (atr[0] == (byte) 0x3B && (atr[1] == (byte) 0x8F || atr[1] == (byte) 0x80)) {
info.setType("MIFARE_CLASSIC");
} else if (atr[0] == (byte) 0x3B && atr[1] == (byte) 0x88) {
info.setType("MIFARE_ULTRALIGHT");
} else {
info.setType("ISO14443A");
}
}
}
}
private void analyzeSelectResponse(byte[] response, NFCCardInfo info) {
if (response.length >= 2) {
int sw1 = response[response.length - 2] & 0xFF;
int sw2 = response[response.length - 1] & 0xFF;
info.setStatusWord((sw1 << 8) | sw2);
if (sw1 == 0x90 && sw2 == 0x00) {
info.setStatus("SUCCESS");
} else {
info.setStatus("ERROR: " + String.format("%02X%02X", sw1, sw2));
}
}
}
private byte[] sendAPDU(CardChannel channel, byte[] command) {
try {
CommandAPDU apdu = new CommandAPDU(command);
ResponseAPDU response = channel.transmit(apdu);
return response.getBytes();
} catch (CardException e) {
System.err.println("APDU error: " + e.getMessage());
return null;
}
}
public void addEventListener(NFCEventListener listener) {
listeners.add(listener);
}
public void removeEventListener(NFCEventListener listener) {
listeners.remove(listener);
}
public List<String> getTerminalNames() {
List<String> names = new ArrayList<>();
for (CardTerminal terminal : terminals) {
names.add(terminal.getName());
}
return names;
}
// Utility method
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X", b));
}
return sb.toString();
}
public static void main(String[] args) {
AdvancedNFCManager manager = new AdvancedNFCManager();
// Add event listener
manager.addEventListener(new NFCEventListener() {
@Override
public void onCardDetected(NFCCardInfo cardInfo) {
System.out.println("EVENT: Card detected - " + cardInfo);
}
@Override
public void onCardRemoved(String terminalName) {
System.out.println("EVENT: Card removed from " + terminalName);
}
@Override
public void onError(String errorMessage) {
System.err.println("EVENT: Error - " + errorMessage);
}
});
// Start monitoring
manager.startMonitoring();
// Keep running for 5 minutes
try {
Thread.sleep(300000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
manager.stopMonitoring();
}
}
// Supporting classes and interfaces
interface NFCEventListener {
void onCardDetected(NFCCardInfo cardInfo);
void onCardRemoved(String terminalName);
void onError(String errorMessage);
}
interface NFCCardHandler {
void handleCard(Card card, NFCCardInfo cardInfo);
}
class NFCCardInfo {
private String terminalName;
private String protocol;
private String type;
private String atrHex;
private int statusWord;
private String status;
private Map<String, Object> additionalInfo;
public NFCCardInfo() {
this.additionalInfo = new HashMap<>();
}
// Getters and setters
public String getTerminalName() { return terminalName; }
public void setTerminalName(String terminalName) { this.terminalName = terminalName; }
public String getProtocol() { return protocol; }
public void setProtocol(String protocol) { this.protocol = protocol; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getAtrHex() { return atrHex; }
public void setAtrHex(String atrHex) { this.atrHex = atrHex; }
public int getStatusWord() { return statusWord; }
public void setStatusWord(int statusWord) { this.statusWord = statusWord; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Map<String, Object> getAdditionalInfo() { return additionalInfo; }
public void setAdditionalInfo(Map<String, Object> additionalInfo) { this.additionalInfo = additionalInfo; }
@Override
public String toString() {
return String.format("NFCCardInfo{terminal='%s', type='%s', protocol='%s', status='%s'}",
terminalName, type, protocol, status);
}
}
// Card handler implementations
class MifareClassicHandler implements NFCCardHandler {
@Override
public void handleCard(Card card, NFCCardInfo cardInfo) {
System.out.println("Handling MIFARE Classic card");
try {
CardChannel channel = card.getBasicChannel();
// MIFARE Classic specific operations
// Note: Actual implementation would require proper authentication
// Try to read first block
byte[] readCommand = new byte[]{
(byte) 0xFF, (byte) 0xB0, (byte) 0x00, (byte) 0x00, (byte) 0x10
};
byte[] response = sendAPDU(channel, readCommand);
if (response != null && response.length > 2) {
byte[] data = Arrays.copyOf(response, response.length - 2);
cardInfo.getAdditionalInfo().put("block_0_data", bytesToHex(data));
System.out.println("Block 0 data: " + bytesToHex(data));
}
} catch (Exception e) {
System.err.println("Error handling MIFARE Classic: " + e.getMessage());
}
}
private byte[] sendAPDU(CardChannel channel, byte[] command) {
try {
CommandAPDU apdu = new CommandAPDU(command);
ResponseAPDU response = channel.transmit(apdu);
return response.getBytes();
} catch (CardException e) {
return null;
}
}
}
class MifareUltralightHandler implements NFCCardHandler {
@Override
public void handleCard(Card card, NFCCardInfo cardInfo) {
System.out.println("Handling MIFARE Ultralight card");
try {
CardChannel channel = card.getBasicChannel();
// Read first few pages
for (int page = 0; page < 4; page++) {
byte[] readCommand = new byte[]{
(byte) 0xFF, (byte) 0xB0, (byte) 0x00, (byte) page, (byte) 0x10
};
byte[] response = sendAPDU(channel, readCommand);
if (response != null && response.length > 2) {
byte[] data = Arrays.copyOf(response, response.length - 2);
cardInfo.getAdditionalInfo().put("page_" + page, bytesToHex(data));
System.out.println("Page " + page + ": " + bytesToHex(data));
}
}
} catch (Exception e) {
System.err.println("Error handling MIFARE Ultralight: " + e.getMessage());
}
}
private byte[] sendAPDU(CardChannel channel, byte[] command) {
try {
CommandAPDU apdu = new CommandAPDU(command);
ResponseAPDU response = channel.transmit(apdu);
return response.getBytes();
} catch (CardException e) {
return null;
}
}
}
class ISO14443AHandler implements NFCCardHandler {
@Override
public void handleCard(Card card, NFCCardInfo cardInfo) {
System.out.println("Handling ISO14443A card");
// Generic ISO14443A handling
}
}
class NDEFHandler implements NFCCardHandler {
@Override
public void handleCard(Card card, NFCCardInfo cardInfo) {
System.out.println("Handling NDEF card");
// NDEF message parsing and handling
}
}
class UnknownCardHandler implements NFCCardHandler {
@Override
public void handleCard(Card card, NFCCardInfo cardInfo) {
System.out.println("Handling unknown card type");
// Generic handling for unknown cards
}
}
// Utility method
class NFCUtils {
public static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X ", b));
}
return sb.toString().trim();
}
}
NDEF Message Processing
Example 3: NDEF Message Reader/Writer
import java.nio.charset.StandardCharsets;
import java.util.*;
public class NDEFProcessor {
public static class NDEFMessage {
private List<NDEFRecord> records;
public NDEFMessage() {
this.records = new ArrayList<>();
}
public void addRecord(NDEFRecord record) {
records.add(record);
}
public List<NDEFRecord> getRecords() {
return records;
}
public byte[] toBytes() {
// Simplified NDEF message encoding
List<byte[]> recordBytes = new ArrayList<>();
int totalLength = 0;
for (int i = 0; i < records.size(); i++) {
NDEFRecord record = records.get(i);
byte[] recordData = record.toBytes(i == 0, i == records.size() - 1);
recordBytes.add(recordData);
totalLength += recordData.length;
}
byte[] message = new byte[totalLength];
int position = 0;
for (byte[] recordData : recordBytes) {
System.arraycopy(recordData, 0, message, position, recordData.length);
position += recordData.length;
}
return message;
}
public static NDEFMessage fromBytes(byte[] data) {
NDEFMessage message = new NDEFMessage();
int position = 0;
while (position < data.length) {
NDEFRecord record = NDEFRecord.fromBytes(data, position);
if (record != null) {
message.addRecord(record);
position += record.getRecordLength();
} else {
break;
}
}
return message;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("NDEF Message with ").append(records.size()).append(" records:\n");
for (int i = 0; i < records.size(); i++) {
sb.append("Record ").append(i).append(": ").append(records.get(i)).append("\n");
}
return sb.toString();
}
}
public static class NDEFRecord {
private boolean messageBegin;
private boolean messageEnd;
private boolean chunked;
private boolean shortRecord;
private boolean idLengthPresent;
private int typeNameFormat;
private int payloadLength;
private byte[] type;
private byte[] id;
private byte[] payload;
public NDEFRecord(int typeNameFormat, byte[] type, byte[] payload) {
this.typeNameFormat = typeNameFormat;
this.type = type;
this.payload = payload;
this.payloadLength = payload.length;
this.id = new byte[0];
}
public byte[] toBytes(boolean isFirst, boolean isLast) {
messageBegin = isFirst;
messageEnd = isLast;
shortRecord = payloadLength <= 255;
idLengthPresent = id.length > 0;
int recordLength = 1 + 1 + type.length + payloadLength; // Header + type length + type + payload
if (idLengthPresent) recordLength += 1 + id.length; // ID length + ID
if (!shortRecord) recordLength += 4; // Extended payload length
byte[] record = new byte[recordLength];
int position = 0;
// Record header
byte header = (byte) (
(messageBegin ? 0x80 : 0x00) |
(messageEnd ? 0x40 : 0x00) |
(chunked ? 0x20 : 0x00) |
(shortRecord ? 0x10 : 0x00) |
(idLengthPresent ? 0x08 : 0x00) |
(typeNameFormat & 0x07)
);
record[position++] = header;
// Type length
record[position++] = (byte) type.length;
// Payload length
if (shortRecord) {
record[position++] = (byte) payloadLength;
} else {
record[position++] = (byte) 0xFF;
// 4-byte payload length (big-endian)
record[position++] = (byte) ((payloadLength >> 24) & 0xFF);
record[position++] = (byte) ((payloadLength >> 16) & 0xFF);
record[position++] = (byte) ((payloadLength >> 8) & 0xFF);
record[position++] = (byte) (payloadLength & 0xFF);
}
// ID length (if present)
if (idLengthPresent) {
record[position++] = (byte) id.length;
}
// Type
System.arraycopy(type, 0, record, position, type.length);
position += type.length;
// ID (if present)
if (idLengthPresent) {
System.arraycopy(id, 0, record, position, id.length);
position += id.length;
}
// Payload
System.arraycopy(payload, 0, record, position, payloadLength);
return record;
}
public static NDEFRecord fromBytes(byte[] data, int start) {
if (data.length - start < 3) return null;
NDEFRecord record = new NDEFRecord();
int position = start;
// Parse header
byte header = data[position++];
record.messageBegin = (header & 0x80) != 0;
record.messageEnd = (header & 0x40) != 0;
record.chunked = (header & 0x20) != 0;
record.shortRecord = (header & 0x10) != 0;
record.idLengthPresent = (header & 0x08) != 0;
record.typeNameFormat = header & 0x07;
// Type length
int typeLength = data[position++] & 0xFF;
// Payload length
if (record.shortRecord) {
record.payloadLength = data[position++] & 0xFF;
} else {
position++; // Skip 0xFF
record.payloadLength = ((data[position++] & 0xFF) << 24) |
((data[position++] & 0xFF) << 16) |
((data[position++] & 0xFF) << 8) |
(data[position++] & 0xFF);
}
// ID length
int idLength = 0;
if (record.idLengthPresent) {
idLength = data[position++] & 0xFF;
}
// Type
record.type = new byte[typeLength];
System.arraycopy(data, position, record.type, 0, typeLength);
position += typeLength;
// ID
if (idLength > 0) {
record.id = new byte[idLength];
System.arraycopy(data, position, record.id, 0, idLength);
position += idLength;
} else {
record.id = new byte[0];
}
// Payload
record.payload = new byte[record.payloadLength];
System.arraycopy(data, position, record.payload, 0, record.payloadLength);
return record;
}
public int getRecordLength() {
int length = 1 + 1 + type.length + payloadLength; // Header + type length + type + payload
if (idLengthPresent) length += 1 + id.length;
if (!shortRecord) length += 4;
return length;
}
// Factory methods for common record types
public static NDEFRecord createTextRecord(String text, String language) {
byte[] languageBytes = language.getBytes(StandardCharsets.US_ASCII);
byte[] textBytes = text.getBytes(StandardCharsets.UTF_8);
byte[] payload = new byte[1 + languageBytes.length + textBytes.length];
payload[0] = (byte) languageBytes.length; // Status byte with language code length
System.arraycopy(languageBytes, 0, payload, 1, languageBytes.length);
System.arraycopy(textBytes, 0, payload, 1 + languageBytes.length, textBytes.length);
return new NDEFRecord(1, "T".getBytes(StandardCharsets.US_ASCII), payload);
}
public static NDEFRecord createUriRecord(String uri) {
byte[] uriBytes = uri.getBytes(StandardCharsets.UTF_8);
byte[] payload = new byte[1 + uriBytes.length];
payload[0] = 0x00; // No abbreviation
System.arraycopy(uriBytes, 0, payload, 1, uriBytes.length);
return new NDEFRecord(1, "U".getBytes(StandardCharsets.US_ASCII), payload);
}
public String getText() {
if (typeNameFormat == 1 && Arrays.equals(type, "T".getBytes(StandardCharsets.US_ASCII))) {
if (payload.length > 0) {
int languageLength = payload[0] & 0x3F;
return new String(payload, 1 + languageLength, payload.length - 1 - languageLength, StandardCharsets.UTF_8);
}
}
return null;
}
public String getUri() {
if (typeNameFormat == 1 && Arrays.equals(type, "U".getBytes(StandardCharsets.US_ASCII))) {
if (payload.length > 0) {
return new String(payload, 1, payload.length - 1, StandardCharsets.UTF_8);
}
}
return null;
}
@Override
public String toString() {
String typeStr = new String(type, StandardCharsets.US_ASCII);
switch (typeNameFormat) {
case 1: // NFC Forum well-known type
if ("T".equals(typeStr)) {
return "Text: " + getText();
} else if ("U".equals(typeStr)) {
return "URI: " + getUri();
}
break;
case 2: // Media-type
return "MIME: " + typeStr + " (" + payload.length + " bytes)";
}
return String.format("Type: %s, TNF: %d, Payload: %d bytes", typeStr, typeNameFormat, payloadLength);
}
// Default constructor for parsing
private NDEFRecord() {}
}
public static void main(String[] args) {
// Create NDEF message with text and URI records
NDEFMessage message = new NDEFMessage();
message.addRecord(NDEFRecord.createTextRecord("Hello NFC!", "en"));
message.addRecord(NDEFRecord.createUriRecord("https://example.com"));
// Convert to bytes
byte[] messageBytes = message.toBytes();
System.out.println("NDEF Message bytes: " + NFCUtils.bytesToHex(messageBytes));
// Parse back from bytes
NDEFMessage parsedMessage = NDEFMessage.fromBytes(messageBytes);
System.out.println(parsedMessage);
}
}
Maven Dependencies and Configuration
pom.xml for NFC Project
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>nfc-reader</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- SmartCard IO (included in JDK) --> <!-- No additional dependency needed --> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.7</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20231013</version> </dependency> <!-- Apache Commons --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.13.0</version> </dependency> <!-- Testing --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>11</source> <target>11</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.example.nfc.NFCMain</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
Platform-Specific Considerations
Windows Setup
public class WindowsNFCConfig {
// PC/SC readers on Windows
static {
// May need to load native libraries
System.loadLibrary("winscard");
}
public static void configureWindowsReader() {
// Windows-specific configuration
System.setProperty("sun.security.smartcardio.library", "winscard");
}
}
Linux Setup
public class LinuxNFCConfig {
static {
// Linux PC/SC configuration
System.setProperty("sun.security.smartcardio.library", "/usr/lib/x86_64-linux-gnu/libpcsclite.so.1");
}
public static void checkPermissions() {
// Ensure user has access to PC/SC daemon
File pcscdSocket = new File("/var/run/pcscd/pcscd.comm");
if (!pcscdSocket.canRead()) {
System.err.println("Warning: May not have access to PC/SC daemon");
System.err.println("Consider adding user to pcscd group");
}
}
}
Security Considerations
Example 4: Secure NFC Operations
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.security.*;
public class SecureNFCOperations {
private final KeyPair cryptoKeyPair;
private final Mac hmac;
public SecureNFCOperations() throws NoSuchAlgorithmException {
// Generate crypto keys for secure operations
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
this.cryptoKeyPair = keyGen.generateKeyPair();
// Initialize HMAC for message authentication
this.hmac = Mac.getInstance("HmacSHA256");
}
public byte[] generateSecureAPDU(byte[] command, byte[] key) throws InvalidKeyException {
// Add MAC to command for authentication
hmac.init(new SecretKeySpec(key, "HmacSHA256"));
byte[] mac = hmac.doFinal(command);
// Combine command and MAC
byte[] secureCommand = new byte[command.length + mac.length];
System.arraycopy(command, 0, secureCommand, 0, command.length);
System.arraycopy(mac, 0, secureCommand, command.length, mac.length);
return secureCommand;
}
public boolean verifyResponse(byte[] response, byte[] expectedMac, byte[] key)
throws InvalidKeyException {
if (response.length < expectedMac.length) return false;
hmac.init(new SecretKeySpec(key, "HmacSHA256"));
byte[] actualMac = hmac.doFinal(response);
return MessageDigest.isEqual(expectedMac, actualMac);
}
public byte[] encryptData(byte[] data, PublicKey key) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(data);
}
public byte[] decryptData(byte[] encryptedData) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, cryptoKeyPair.getPrivate());
return cipher.doFinal(encryptedData);
}
public static class SecureNFCSession {
private final byte[] sessionKey;
private final long sessionStart;
private final String sessionId;
public SecureNFCSession(byte[] sessionKey) {
this.sessionKey = sessionKey.clone();
this.sessionStart = System.currentTimeMillis();
this.sessionId = generateSessionId();
}
public byte[] getSessionKey() {
return sessionKey.clone();
}
public boolean isValid() {
// Session expires after 5 minutes
return System.currentTimeMillis() - sessionStart < 300000;
}
private String generateSessionId() {
return "SESS_" + System.currentTimeMillis() + "_" +
new Random().nextInt(10000);
}
}
}
Testing and Debugging
Example 5: NFC Test Suite
import org.junit.*;
import static org.junit.Assert.*;
public class NFCTestSuite {
private AdvancedNFCManager nfcManager;
@Before
public void setUp() {
nfcManager = new AdvancedNFCManager();
}
@Test
public void testTerminalDiscovery() {
List<String> terminals = nfcManager.getTerminalNames();
assertNotNull("Terminal list should not be null", terminals);
// Note: Test will fail if no NFC reader is connected
}
@Test
public void testNDEFRecordCreation() {
NDEFProcessor.NDEFRecord textRecord =
NDEFProcessor.NDEFRecord.createTextRecord("Test", "en");
assertNotNull("Text record should be created", textRecord);
assertEquals("Text should match", "Test", textRecord.getText());
NDEFProcessor.NDEFRecord uriRecord =
NDEFProcessor.NDEFRecord.createUriRecord("https://test.com");
assertNotNull("URI record should be created", uriRecord);
assertEquals("URI should match", "https://test.com", uriRecord.getUri());
}
@Test
public void testNDEFMessageEncoding() {
NDEFProcessor.NDEFMessage message = new NDEFProcessor.NDEFMessage();
message.addRecord(NDEFProcessor.NDEFRecord.createTextRecord("Hello", "en"));
byte[] encoded = message.toBytes();
assertNotNull("Encoded message should not be null", encoded);
assertTrue("Encoded message should not be empty", encoded.length > 0);
NDEFProcessor.NDEFMessage decoded = NDEFProcessor.NDEFMessage.fromBytes(encoded);
assertNotNull("Decoded message should not be null", decoded);
assertEquals("Should have one record", 1, decoded.getRecords().size());
}
@After
public void tearDown() {
if (nfcManager != null) {
nfcManager.stopMonitoring();
}
}
}
Conclusion
Key Features Implemented
- Basic NFC Reading: Card detection and communication
- Advanced Card Handling: Support for multiple card types
- NDEF Processing: Standard NFC data format support
- Security: Secure operations and authentication
- Event Management: Asynchronous card detection
- Cross-Platform Support: Windows, Linux, macOS
Common Use Cases
- Access Control: Employee badges, building access
- Payment Systems: Contactless payments
- Inventory Management: Product tracking with NFC tags
- Smart Posters: Interactive advertising
- Data Exchange: Contactless file sharing
- Authentication: Two-factor authentication
Best Practices
- Error Handling: Robust exception handling for card operations
- Resource Management: Proper connection cleanup
- Security: Implement authentication and encryption
- Performance: Asynchronous operations for better responsiveness
- Compatibility: Support for multiple card types and standards
This comprehensive NFC implementation provides a solid foundation for building Java applications that interact with NFC tags and readers across different platforms.