Java Card Applet Tutorial: Build Your First Smart Card Application
What you'll learn: Write a complete Java Card applet from scratch, handle APDU commands, compile to a CAP file, and install it on a smart card using GlobalPlatform. All code examples are tested on Java Card 3.0.4 Classic.
1. Java Card Platform Overview
Java Card is a software platform that enables Java-based applications (called applets) to run on resource-constrained smart cards and secure elements. Unlike standard Java, Java Card uses a subset of the language with no threads, no garbage collection (on Classic edition), and a strict sandboxed security model where each applet is isolated in its own security domain.
| Feature | Standard Java (J2SE) | Java Card |
|---|---|---|
| Heap size | Hundreds of MB | ~2-128 KB |
| Data types | int, long, double, etc. | byte, short (no long, float, double in Classic) |
| Threads | Full multi-threading | Single-threaded |
| GC | Generational GC | Optional (Classic: manual memory mgmt) |
| Class libraries | Thousands | javacard.framework + extensions |
| Deployment format | JAR (class files) | CAP (Converted Applet) file |
| Lifecycle | JVM start/stop | Persistent: applets survive power cycles |
2. Development Environment Setup
Option A: JCIDE (Recommended for Beginners)
JCIDE (Java Card Integrated Development Environment) is a free, all-in-one IDE that includes the Java Card SDK, simulator, CAP converter, and debugger. Download from the JavaCardOS Tools page.
Option B: Eclipse + Java Card SDK
- Install Eclipse IDE for Java
- Download Java Card SDK (e.g., Oracle Java Card 3.0.5 or NXP JCOP SDK)
- Configure the converter tool path in Eclipse build settings
- Use GlobalPlatformPro (gp.jar/gp.exe) for CAP installation
Pro tip: Use GlobalPlatformPro (
gp --install applet.cap) to load CAP files onto real cards. It's the de facto standard open-source tool for GP card management. Install with: brew install gp (macOS) or download from GitHub.
3. Your First Applet: "Hello Card"
package com.cardwise.hellocard;
import javacard.framework.*;
public class HelloCardApplet extends Applet {
// APDU instruction codes
private static final byte INS_SAY_HELLO = (byte) 0x10;
private static final byte INS_SET_NAME = (byte) 0x20;
// Applet data (persistent across power cycles)
private byte[] storedName;
private static final short MAX_NAME_LEN = 32;
/**
* Called once when applet is installed on the card.
* Use this to allocate persistent memory and set initial state.
*/
public static void install(byte[] bArray, short bOffset, byte bLength) {
// Create applet instance and register with JCRE
HelloCardApplet applet = new HelloCardApplet();
applet.storedName = new byte[MAX_NAME_LEN];
applet.register(bArray, (short)(bOffset + 1), bArray[bOffset]);
}
/**
* APDU dispatcher — called for every incoming APDU command.
*/
public void process(APDU apdu) {
byte[] buffer = apdu.getBuffer();
// Only process SELECT commands at the applet level
if (selectingApplet()) {
return;
}
// Parse the APDU header
byte cla = buffer[ISO7816.OFFSET_CLA];
byte ins = buffer[ISO7816.OFFSET_INS];
if (cla != 0x00) {
ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
}
switch (ins) {
case INS_SAY_HELLO:
handleSayHello(apdu);
break;
case INS_SET_NAME:
handleSetName(apdu);
break;
default:
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
}
}
/**
* Return a greeting string.
* APDU: 00 10 00 00 [Le]
*/
private void handleSayHello(APDU apdu) {
byte[] buffer = apdu.getBuffer();
short le = apdu.setOutgoing();
// Build response: "Hello, [name]!"
byte[] greeting = {'H','e','l','l','o',',',' '};
short len = (short) greeting.length;
Util.arrayCopyNonAtomic(greeting, (short) 0, buffer, (short) 0, len);
// Append stored name
short nameLen = 0;
for (short i = 0; i < storedName.length && storedName[i] != 0; i++) {
nameLen++;
}
if (nameLen > 0) {
Util.arrayCopyNonAtomic(storedName, (short) 0, buffer, len, nameLen);
len += nameLen;
}
buffer[len++] = '!';
apdu.setOutgoingLength(len);
apdu.sendBytes((short) 0, len);
}
/**
* Store a name sent in the APDU data field.
* APDU: 00 20 00 00 [Lc] [name bytes]
*/
private void handleSetName(APDU apdu) {
byte[] buffer = apdu.getBuffer();
short bytesRead = apdu.setIncomingAndReceive();
short dataOffset = apdu.getOffsetCdata();
// Copy name data into persistent storage
short copyLen = bytesRead < MAX_NAME_LEN ? bytesRead : MAX_NAME_LEN;
Util.arrayCopyNonAtomic(buffer, dataOffset, storedName, (short) 0, copyLen);
// Zero-fill remaining bytes
for (short i = copyLen; i < MAX_NAME_LEN; i++) {
storedName[i] = 0;
}
}
}
4. APDU Handling Patterns
Case 2: Incoming Data, No Outgoing Data
// APDU: CLA INS P1 P2 Lc Data
// Typical for: VERIFY, UPDATE BINARY, PUT DATA
short bytesRead = apdu.setIncomingAndReceive();
short offset = apdu.getOffsetCdata();
// ... process data at buffer[offset] ...
// No send — just success (SW=9000 implicit)
Case 4: Incoming Data + Outgoing Data
// APDU: CLA INS P1 P2 Lc Data Le
// Typical for: SELECT, INTERNAL AUTHENTICATE
short bytesRead = apdu.setIncomingAndReceive();
// ... process input data ...
short le = apdu.setOutgoing();
// ... fill buffer[0..le-1] with response data ...
apdu.setOutgoingLength(responseLen);
apdu.sendBytes((short) 0, responseLen);
5. Compilation & CAP File Generation
# Step 1: Compile .java to .class (standard javac with JC SDK)
javac -cp javacard.jar -d bin/ src/com/cardwise/hellocard/*.java
# Step 2: Convert .class to .cap (Java Card converter tool)
converter -classdir bin/ \
-applet 0x01:0x02:0x03:0x04:0x05:0x01 \
-package 0x01:0x02:0x03:0x04:0x05 \
-out CAP \
com.cardwise.hellocard 0x01:0x02:0x03:0x04:0x05 1.0
# Step 3: Install CAP on card via GlobalPlatform
gp --install hellocard.cap
# Step 4: Verify installation
gp --list
6. Key Java Card API Classes
| Class | Package | Purpose |
|---|---|---|
Applet | javacard.framework | Base class for all applets. Implements install() and process(). |
APDU | javacard.framework | Wraps incoming/outgoing APDU buffer. Core I/O class. |
ISO7816 | javacard.framework | Constants: SW values, CLA/INS/P1/P2 offsets. |
ISOException | javacard.framework | Throw with SW1 SW2 to signal errors. throwIt(SW_FILE_NOT_FOUND). |
Util | javacard.framework | Array copy/compare utilities (no System.arraycopy()). |
OwnerPIN | javacard.framework | PIN management: check(), update(), getTriesRemaining(). |
Signature | javacard.security | On-card digital signatures (RSA/ECC). |
KeyBuilder | javacard.security | Create DES/AES/RSA/ECC key objects on the card. |
Cipher | javacardx.crypto | On-card encryption/decryption (AES-CBC, DES-CBC, etc.). |
7. Real-World Deployment Checklist
- Key diversification: Use a KDF to derive per-card applet keys from a master key + card serial number
- Secure messaging: Use SCP02 or SCP03 for all personalization APDUs
- Transaction atomicity: Use
JCSystem.beginTransaction()/commitTransaction()for multi-step state changes - Applet firewall: Use
Shareableinterface for inter-applet communication - Memory budget: Monitor transient vs persistent memory usage. Use
JCSystem.getAvailableMemory()
Related reading: ISO 7816 Complete Guide for the underlying APDU protocol, SCP02 vs SCP03 for secure channel setup during applet installation.