Every conversation with a smart card is a sequence of APDUs (application protocol data units): the host sends a command APDU, the card answers with a response APDU, and the two status bytes at the end of each response tell you what happened. This reference explains the byte layout of both, the four command cases, short and extended length, a selection of the commands developers meet most often, and the status words worth handling. It ends with a small, tested JavaScript module for building and parsing APDUs, cross-checked against Java’s own implementation.
Scope and sources. The command structure used by smart cards is defined in ISO/IEC 7816-4, which is a paid standard and is not quoted here. Everything below is taken from openly published documents that describe the same mechanisms for their own applications: ETSI TS 102 221 (the SIM/UICC interface), NIST SP 800-73-5 (the US PIV card), Oracle’s Java SE and Java Card API documentation and the Android NFC documentation. Treat this page as a practical guide, not as a normative specification; the specification of the application you talk to always has the final word.
Command APDU structure
A command APDU has a mandatory four-byte header and an optional body:
| CLA | INS | P1 | P2 | Lc | Data (Nc bytes) | Le |
|<------ header ----->|<------------ body ------------->|
1 1 1 1 0,1,3 0..65535 0..3 bytes
| Field | Size | Meaning |
|---|---|---|
| CLA | 1 byte | Class: whether the command is interindustry or proprietary, plus logical channel, secure messaging and command chaining indicators |
| INS | 1 byte | Instruction: which command this is (for example A4 = SELECT) |
| P1, P2 | 1 byte each | Parameters whose meaning depends on the instruction (selection method, file offset, record number, key reference …) |
| Lc | absent, 1 or 3 bytes | Length of the command data, called Nc |
| Data | Nc bytes | Command data (an AID, a PIN, bytes to write …) |
| Le | absent, 1, 2 or 3 bytes | Maximum number of response data bytes expected, called Ne |
Keep the two pairs of names apart: Nc and Ne are the actual numbers of bytes, while Lc and Le are the fields that encode them. The encoding is not always the plain number (an Le byte of 00 means 256, for example), which is why APIs such as Java’s CommandAPDU ask you for Ne rather than for an Le byte.
The four command cases
Whether the body carries data, expects data back, both or neither gives four layouts:
| Case | Layout | Command data? | Response data? | Example |
|---|---|---|---|---|
| 1 | CLA INS P1 P2 |
No | No | PIV VERIFY with no PIN, asking only for the PIN status |
| 2 | CLA INS P1 P2 Le |
No | Yes | READ BINARY, GET RESPONSE |
| 3 | CLA INS P1 P2 Lc Data |
Yes | No | UPDATE BINARY, VERIFY with a PIN |
| 4 | CLA INS P1 P2 Lc Data Le |
Yes | Yes | SELECT by AID with a response requested |
Short and extended length
In the short form, Lc and Le are one byte each. Lc can describe 1 to 255 data bytes, and an Le of 00 means “up to 256 bytes”. When a command needs more than 255 data bytes or more than 256 response bytes, it switches to the extended form: the length fields grow to two bytes, and a 00 byte marks where the extended length starts.
| Case | Short form | Extended form |
|---|---|---|
| 2 | … Le (Le 00 = 256) |
… 00 Le1 Le2 (Le 0000 = 65536) |
| 3 | … Lc Data (Nc 1–255) |
… 00 Lc1 Lc2 Data (Nc 1–65535) |
| 4 | … Lc Data Le |
… 00 Lc1 Lc2 Data Le1 Le2 |
Multi-byte length fields are written most significant byte first (big-endian). Two practical limits matter more than the encoding itself:
- Not every reader or card accepts extended length. Java’s API documentation says so explicitly, and on Android you can ask a contactless card’s connection with
IsoDep.isExtendedLengthApduSupported()before relying on it. - The limits depend on the platform. Java’s host-side
CommandAPDUaccepts Nc up to 65535 and Ne up to 65536. A Java Card applet that implementsjavacardx.apdu.ExtendedLengthcan receive and send up to 32767 bytes of APDU data. Without that interface the applet only sees short APDUs.
The CLA byte
- If bit 8 of CLA is 0 (values
0x00–0x7F), the command is interindustry: its meaning is shared across applications. If bit 8 is 1 (0x80and above), the command is not interindustry, and its meaning comes from the application’s own specification. Payment applications, for example, define several of their commands with CLA80. FFis not a valid CLA value.- The low bits carry the logical channel number (in the first interindustry form, bits 2 and 1 select channel 0 to 3) and the secure-messaging indication.
- Bit 5 (
0x10) marks command chaining: the command is part of a chain and is not the last one. PIV cards use CLA10for every chained command except the last, which uses00. Not every application supports chaining; ETSI TS 102 221 does not.
Response APDU structure
| Data (0..Ne bytes) | SW1 | SW2 |
The response is an optional data field followed by two mandatory status bytes, SW1 and SW2, read together as the status word. 90 00 means normal completion. The status bytes are always the last two bytes: strip them before you parse the data (for example as BER-TLV), or the parser will read them as the start of another object.
Selected APDU commands
The instruction bytes below are published in ETSI TS 102 221 and NIST SP 800-73-5. The table is a starting point, not a complete list: each application defines which commands it supports and what their parameters mean.
| Command | INS | Case | P1 / P2 | What it does |
|---|---|---|---|---|
| SELECT | A4 |
3 or 4 | P1 04 = select by AID (DF name); P2 chooses first/next occurrence and what to return |
Makes an application or file current; the response usually describes it |
| READ BINARY | B0 |
2 | Offset into the current file (P1 bit 8 = 0), or a short file identifier in P1 (bit 8 = 1) | Reads bytes from a transparent file |
| UPDATE BINARY | D6 |
3 | Same coding as READ BINARY | Overwrites bytes in a transparent file |
| READ RECORD | B2 |
2 | P1 = record number, P2 = mode | Reads one record from a record-structured file |
| VERIFY | 20 |
3 (or 1) | P2 = key/PIN reference | Presents a PIN; with no data, PIV returns the PIN status instead |
| GET RESPONSE | C0 |
2 | 00 00 |
Fetches response data the card is holding back (see 61XX below) |
| GET DATA (PIV) | CB |
4 | 3F FF |
Reads a PIV data object named by a tag list in the data field |
A note on GET DATA: PIV uses INS CB, whereas payment cards are seen in published transaction traces using 80 CA with the requested tag in P1–P2. The same INS value can mean different things in different applications (in ETSI TS 102 221, for instance, CB is RETRIEVE DATA), so look the command up in the specification of the application you have selected.
SELECT by AID in practice
The command most programs send first is SELECT by application identifier. For the PIV application it looks like this:
00 A4 04 00 0B A0 00 00 03 08 00 00 10 00 01 00 00
| | | | | |---------- AID (11 bytes) --------| |
CLA INS P1 P2 Lc Le (00 = up to 256)
P2 also selects which match is chosen when several applications start with the same bytes: 00 for the first or only occurrence and 02 for the next one. A card can accept a right-truncated AID (only its first bytes) and select the first application that matches; PIV cards, for example, accept the PIV AID with or without its two version bytes. What the card returns depends on the application: a UICC answers with an FCP template (tag 62), PIV with an application property template (tag 61), and payment applications, in published transaction traces, with an FCI template (tag 6F). Our guides to application discovery and selection and to implementing and troubleshooting application selection go further.
Status words (SW1 SW2)
| SW1 SW2 | Meaning | What to do |
|---|---|---|
90 00 |
Normal completion | Use the data |
61 XX |
Command accepted, XX response bytes are waiting | Send GET RESPONSE with Le = XX (repeat if another 61 XX follows) |
6C XX |
Wrong Le; XX is the exact length available | Resend the same command with Le = XX |
63 CX |
VERIFY failed, X further tries allowed | Stop retrying automatically before the counter reaches zero |
67 00 |
Wrong length | Check Lc, Le and the short/extended form |
69 82 |
Security status not satisfied | Authenticate or verify the PIN first |
69 83 |
Authentication method (for example the PIN) blocked | Needs an unblock procedure defined by the application |
69 85 |
Conditions of use not satisfied | The command is not allowed in the current state |
6A 80 |
Incorrect data in the command data field | Check the data encoding |
6A 81 |
Function not supported | |
6A 82 |
File not found; after SELECT by AID: application not found | Try the next candidate AID |
6A 83 |
Record not found | Check the record number |
6A 86 |
Incorrect P1–P2 | Check the parameters (some cards answer this to an unsupported selection option) |
6A 88 |
Referenced data not found | Check the tag or key reference |
6B 00 |
Wrong P1–P2 | Offset or parameter out of range |
6D 00 |
Instruction not supported or invalid | The selected application does not implement this INS |
6E 00 |
Class not supported | Wrong CLA (for example a proprietary class sent to another application) |
61 XX and 6C XX are different in kind from the rest. In ETSI TS 102 221 they are procedure bytes of the T=0 transmission protocol, used only for case 2 and case 4 commands, and Java’s CardChannel.transmit() handles both automatically. Whether your code ever sees them depends on the reader stack you use, so handle them, but do not treat them as application errors.
Building and parsing APDUs in JavaScript
The module below builds command APDUs in short or extended form (choosing the short form whenever it is enough), decodes a command back into its fields and case, and splits a response into data and status bytes. It has no dependencies and runs in Node.js and modern browsers.
// apdu.mjs — build and parse smart-card APDUs (short and extended length).
// Ambimat Electronics, 2026. Dependency-free ES module; works in Node.js 18+ and modern browsers.
//
// Encoding rules follow the case 1-4 layouts described in the Java SE javax.smartcardio.CommandAPDU
// documentation: Nc (data length) 0..65535, Ne (expected response length) 0..65536; short form when
// Nc <= 255 and Ne <= 256, otherwise extended form. Le = 00 means 256 (short) and 0000 means 65536 (extended).
export class ApduError extends Error {}
export function fromHex(hex) {
if (typeof hex !== 'string') throw new ApduError('hex input must be a string');
const clean = hex.replace(/[\s:]/g, '');
if (clean.length % 2 !== 0 || /[^0-9a-fA-F]/.test(clean)) throw new ApduError(`invalid hex: "${hex}"`);
const out = new Uint8Array(clean.length / 2);
for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(2 * i, 2 * i + 2), 16);
return out;
}
export function toHex(bytes, sep = ' ') {
return Array.from(bytes, (b) => b.toString(16).toUpperCase().padStart(2, '0')).join(sep);
}
function byteField(name, v) {
if (!Number.isInteger(v) || v < 0 || v > 0xff) throw new ApduError(`${name} must be a byte (0..255), got ${v}`);
return v;
}
// Build a command APDU. data: Uint8Array or hex string (optional). ne: expected response bytes (0 = none).
export function encodeCommand({ cla, ins, p1, p2, data = new Uint8Array(0), ne = 0 }) {
const body = typeof data === 'string' ? fromHex(data) : data;
const nc = body.length;
if (nc > 65535) throw new ApduError(`command data too long: ${nc} bytes (max 65535)`);
if (!Number.isInteger(ne) || ne < 0 || ne > 65536) throw new ApduError(`ne must be 0..65536, got ${ne}`);
const header = [byteField('CLA', cla), byteField('INS', ins), byteField('P1', p1), byteField('P2', p2)];
const extended = nc > 255 || ne > 256;
const out = [...header];
if (nc > 0) out.push(...(extended ? [0x00, nc >> 8, nc & 0xff] : [nc]), ...body);
if (ne > 0) {
if (!extended) out.push(ne === 256 ? 0x00 : ne);
else {
const le = ne === 65536 ? 0 : ne;
if (nc === 0) out.push(0x00); // case 2 extended: 00 then two Le bytes
out.push(le >> 8, le & 0xff);
}
}
return Uint8Array.from(out);
}
// Parse a command APDU back into its fields; identifies the case (1-4) and short/extended form.
export function decodeCommand(input) {
const a = typeof input === 'string' ? fromHex(input) : input;
if (a.length < 4) throw new ApduError(`command APDU needs at least 4 header bytes, got ${a.length}`);
const [cla, ins, p1, p2] = a;
const r = { cla, ins, p1, p2, data: a.subarray(4, 4), ne: 0, case: 1, extended: false };
const n = a.length - 4;
if (n === 0) return r;
if (n === 1) return { ...r, case: 2, ne: a[4] === 0 ? 256 : a[4] }; // case 2 short
if (a[4] !== 0) { // short Lc
const nc = a[4];
if (n === 1 + nc) return { ...r, case: 3, data: a.subarray(5, 5 + nc) };
if (n === 2 + nc) return { ...r, case: 4, data: a.subarray(5, 5 + nc), ne: a[5 + nc] === 0 ? 256 : a[5 + nc] };
throw new ApduError(`length mismatch: Lc=${nc} but ${n - 1} bytes follow`);
}
if (n === 3) { // case 2 extended
const le = (a[5] << 8) | a[6];
return { ...r, case: 2, extended: true, ne: le === 0 ? 65536 : le };
}
const nc = (a[5] << 8) | a[6]; // extended Lc
if (nc === 0) throw new ApduError('extended Lc of 0000 is not valid');
if (n === 3 + nc) return { ...r, case: 3, extended: true, data: a.subarray(7, 7 + nc) };
if (n === 5 + nc) {
const le = (a[7 + nc] << 8) | a[8 + nc];
return { ...r, case: 4, extended: true, data: a.subarray(7, 7 + nc), ne: le === 0 ? 65536 : le };
}
throw new ApduError(`length mismatch: extended Lc=${nc} but ${n - 3} bytes follow`);
}
// Split a response APDU into data and the two status bytes (SW1 SW2), which are always the last two.
export function parseResponse(input) {
const r = typeof input === 'string' ? fromHex(input) : input;
if (r.length < 2) throw new ApduError(`response APDU needs at least SW1 SW2, got ${r.length} byte(s)`);
const sw1 = r[r.length - 2], sw2 = r[r.length - 1];
return { data: r.subarray(0, r.length - 2), sw1, sw2, sw: (sw1 << 8) | sw2 };
}
Example: the four cases and extended length
// build-commands.mjs — the four APDU cases, then the same command forced into extended length.
import { encodeCommand, decodeCommand, toHex } from '../apdu.mjs';
const aid = 'A000000308000010000100'; // the PIV application AID (NIST SP 800-73)
const commands = {
'case 1 VERIFY, no PIN (status) ': { cla: 0x00, ins: 0x20, p1: 0x00, p2: 0x80 },
'case 2 READ BINARY, Le=256 ': { cla: 0x00, ins: 0xb0, p1: 0x00, p2: 0x00, ne: 256 },
'case 3 UPDATE BINARY, 2 bytes ': { cla: 0x00, ins: 0xd6, p1: 0x00, p2: 0x10, data: 'CAFE' },
'case 4 SELECT by AID, Le=256 ': { cla: 0x00, ins: 0xa4, p1: 0x04, p2: 0x00, data: aid, ne: 256 },
'case 2E READ BINARY, Le=1000 ': { cla: 0x00, ins: 0xb0, p1: 0x00, p2: 0x00, ne: 1000 },
'case 4E SELECT by AID, Le=65536 ': { cla: 0x00, ins: 0xa4, p1: 0x04, p2: 0x00, data: aid, ne: 65536 },
};
for (const [name, c] of Object.entries(commands)) {
const bytes = encodeCommand(c);
const d = decodeCommand(bytes);
console.log(`${name} ${toHex(bytes)} -> case ${d.case}${d.extended ? ' extended' : ''}, Nc=${d.data.length}, Ne=${d.ne}`);
}
Output:
case 1 VERIFY, no PIN (status) 00 20 00 80 -> case 1, Nc=0, Ne=0
case 2 READ BINARY, Le=256 00 B0 00 00 00 -> case 2, Nc=0, Ne=256
case 3 UPDATE BINARY, 2 bytes 00 D6 00 10 02 CA FE -> case 3, Nc=2, Ne=0
case 4 SELECT by AID, Le=256 00 A4 04 00 0B A0 00 00 03 08 00 00 10 00 01 00 00 -> case 4, Nc=11, Ne=256
case 2E READ BINARY, Le=1000 00 B0 00 00 00 03 E8 -> case 2 extended, Nc=0, Ne=1000
case 4E SELECT by AID, Le=65536 00 A4 04 00 00 00 0B A0 00 00 03 08 00 00 10 00 01 00 00 00 -> case 4 extended, Nc=11, Ne=65536
Example: reading status words
// read-response.mjs — split responses into data + SW1 SW2 and give the status word a first reading.
// Meanings are those published in ETSI TS 102 221 and NIST SP 800-73-5; an application's own
// specification has the final word, and some codes are reused with other meanings.
import { parseResponse, toHex } from '../apdu.mjs';
function describe(sw) {
const sw1 = sw >> 8, sw2 = sw & 0xff;
if (sw === 0x9000) return 'normal completion';
if (sw1 === 0x61) return `more bytes waiting: send GET RESPONSE with Le=${toHex([sw2])} (T=0 transport signal)`;
if (sw1 === 0x6c) return `wrong Le: resend the same command with Le=${toHex([sw2])} (T=0 transport signal)`;
if (sw1 === 0x63 && (sw2 & 0xf0) === 0xc0) return `VERIFY failed, ${sw2 & 0x0f} tries left`;
return {
0x6700: 'wrong length', 0x6982: 'security status not satisfied', 0x6983: 'authentication method blocked',
0x6985: 'conditions of use not satisfied', 0x6a80: 'incorrect data in the command data field',
0x6a81: 'function not supported', 0x6a82: 'file or application not found', 0x6a83: 'record not found',
0x6a86: 'incorrect P1-P2', 0x6a88: 'referenced data not found', 0x6b00: 'wrong P1-P2',
0x6d00: 'instruction (INS) not supported', 0x6e00: 'class (CLA) not supported',
}[sw] ?? 'not in this table: check the specification of the application you selected';
}
for (const hex of ['6F 07 84 05 A0 00 00 00 01 90 00', '61 1C', '6C 10', '63 C2', '6A 82', '6D 00', '91 00']) {
const r = parseResponse(hex);
console.log(`${hex.padEnd(34)} data=${r.data.length} bytes SW=${toHex([r.sw1, r.sw2], '')} ${describe(r.sw)}`);
}
Output:
6F 07 84 05 A0 00 00 00 01 90 00 data=9 bytes SW=9000 normal completion
61 1C data=0 bytes SW=611C more bytes waiting: send GET RESPONSE with Le=1C (T=0 transport signal)
6C 10 data=0 bytes SW=6C10 wrong Le: resend the same command with Le=10 (T=0 transport signal)
63 C2 data=0 bytes SW=63C2 VERIFY failed, 2 tries left
6A 82 data=0 bytes SW=6A82 file or application not found
6D 00 data=0 bytes SW=6D00 instruction (INS) not supported
91 00 data=0 bytes SW=9100 not in this table: check the specification of the application you selected
Cross-check in Java
Java’s javax.smartcardio builds the same bytes, which is a quick way to check any APDU library. This needs only a JDK, not a reader:
// BuildSelect.java — the same SELECT command built with Java's javax.smartcardio (JDK 11+), no reader needed.
// java BuildSelect.java
import javax.smartcardio.CommandAPDU;
import java.util.HexFormat;
public class BuildSelect {
public static void main(String[] args) {
byte[] aid = HexFormat.of().parseHex("A000000308000010000100");
CommandAPDU select = new CommandAPDU(0x00, 0xA4, 0x04, 0x00, aid, 256);
System.out.println(HexFormat.ofDelimiter(" ").withUpperCase().formatHex(select.getBytes()));
System.out.println("Nc=" + select.getNc() + " Ne=" + select.getNe());
}
}
It prints 00 A4 04 00 0B A0 00 00 03 08 00 00 10 00 01 00 00 and Nc=11 Ne=256, matching the JavaScript output above.
On the card side: Java Card
If you write the applet that answers these commands, Java Card’s javacard.framework API gives you the same structure from the other end:
| API | Use |
|---|---|
ISO7816.OFFSET_CLA, OFFSET_INS, OFFSET_P1, OFFSET_P2, OFFSET_LC, OFFSET_CDATA |
Byte positions 0 to 5 in the APDU buffer (OFFSET_EXT_CDATA = 7 for extended length) |
APDU.setIncomingAndReceive() |
Receives the command data of a case 3 or 4 command into the buffer |
APDU.setOutgoing(), setOutgoingLength(), sendBytes() or setOutgoingAndSend() |
Switches to outbound, learns Ne, sets the actual response length and sends the data |
ISOException.throwIt(sw) |
Ends the command with a status word, for example ISO7816.SW_WRONG_LENGTH (0x6700) |
javacardx.apdu.ExtendedLength |
Tagging interface an applet implements to receive and send extended-length APDUs (up to 32767 data bytes) |
The APDU buffer is at least 133 bytes long (5 header bytes plus 128 data bytes), so an applet that expects long commands must receive them in pieces or declare extended-length support.
Common mistakes
- Wrong Lc or Le. Lc must equal the number of data bytes exactly. An Le that is too small or too large can produce
67 00or6C XX. - Counting hex characters instead of bytes.
"A0000000031010"is fourteen characters but seven bytes. Lc is the byte count. - Treating SW1 SW2 as data. The status word belongs to the transport, not to the payload. Remove it before parsing TLV data.
- Byte order in multi-byte fields. Extended Lc and Le are big-endian, and so are most multi-byte values inside smart-card data. See our explanation of big- and little-endian byte order.
- Assuming
90 00is the only answer worth handling.61 XX,6C XXand63 CXneed specific reactions, and6A 82after a SELECT is a normal result when you probe several AIDs. - Assuming extended length or chaining is available. Check the reader, the card and the application before relying on either.
- Reusing an INS value across applications. Instruction meanings depend on the CLA and on the selected application.
How the code was tested
The test suite compares the module against Java’s own javax.smartcardio.CommandAPDU: a small Java program (included with our engineering notes) generated 2,000 commands covering every case and the boundary lengths 0, 1, 255, 256, 257, 65535 and 65536, and the JavaScript output must equal the JDK’s bytes, while the decoder must recover the same Nc and Ne the JDK reports. The suite also checks the four cases byte for byte, the switch to extended form only when needed, response splitting, and rejection of invalid input:
// apdu.test.mjs — node --test apdu.test.mjs (APDU_IMPL=<path> tests another copy, e.g. code extracted from the live page)
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
const impl = process.env.APDU_IMPL ? new URL(process.env.APDU_IMPL, `file://${process.cwd()}/`).href : './apdu.mjs';
const { encodeCommand, decodeCommand, parseResponse, fromHex, toHex, ApduError } = await import(impl);
const data = (nc, seed) => Uint8Array.from({ length: nc }, (_, i) => (i * 31 + seed) & 0xff);
const vectors = readFileSync(new URL('./conformance/vectors.jsonl', import.meta.url), 'utf8').trim().split('\n').map((l) => JSON.parse(l));
test('encoding matches the JDK javax.smartcardio.CommandAPDU on 2,000 vectors', () => {
for (const v of vectors) {
const b = encodeCommand({ cla: v.cla, ins: v.ins, p1: v.p1, p2: v.p2, data: data(v.nc, v.seed), ne: v.ne });
if (v.hex) assert.equal(toHex(b, ''), v.hex, JSON.stringify(v));
else {
assert.equal(b.length, v.len, JSON.stringify(v));
assert.equal(createHash('sha256').update(b).digest('hex').toUpperCase(), v.sha256, JSON.stringify(v));
}
const d = decodeCommand(b);
assert.deepEqual([d.cla, d.ins, d.p1, d.p2, d.data.length, d.ne], [v.cla, v.ins, v.p1, v.p2, v.jdk_nc, v.jdk_ne]);
assert.deepEqual(Array.from(d.data), Array.from(data(v.nc, v.seed)));
}
});
test('the four cases, short form', () => {
assert.equal(toHex(encodeCommand({ cla: 0, ins: 0xa4, p1: 4, p2: 0 })), '00 A4 04 00'); // case 1
assert.equal(toHex(encodeCommand({ cla: 0, ins: 0xb0, p1: 0, p2: 0, ne: 256 })), '00 B0 00 00 00'); // case 2, Le 00 = 256
assert.equal(toHex(encodeCommand({ cla: 0, ins: 0xd6, p1: 0, p2: 0, data: '0102' })), '00 D6 00 00 02 01 02'); // case 3
assert.equal(toHex(encodeCommand({ cla: 0, ins: 0xa4, p1: 4, p2: 0, data: 'A000000308000010000100', ne: 256 })),
'00 A4 04 00 0B A0 00 00 03 08 00 00 10 00 01 00 00'); // case 4
assert.deepEqual([1, 2, 3, 4].map((c) => decodeCommand(['00A40400', '00B0000000', '00D60000020102', '00A404000BA00000030800001000010000'][c - 1]).case), [1, 2, 3, 4]);
});
test('extended form is used only when needed', () => {
assert.equal(toHex(encodeCommand({ cla: 0, ins: 0xb0, p1: 0, p2: 0, ne: 257 })), '00 B0 00 00 00 01 01');
assert.equal(toHex(encodeCommand({ cla: 0, ins: 0xb0, p1: 0, p2: 0, ne: 65536 })), '00 B0 00 00 00 00 00');
const big = encodeCommand({ cla: 0, ins: 0xd6, p1: 0, p2: 0, data: new Uint8Array(256) });
assert.equal(toHex(big.subarray(0, 7)), '00 D6 00 00 00 01 00');
const c4 = encodeCommand({ cla: 0, ins: 0x2a, p1: 0x9e, p2: 0x9a, data: new Uint8Array(10), ne: 300 });
assert.equal(toHex(c4), '00 2A 9E 9A 00 00 0A ' + '00 '.repeat(10) + '01 2C');
assert.equal(decodeCommand(c4).extended, true);
});
test('responses: status bytes are always the last two', () => {
const r = parseResponse('6F 07 84 05 A0 00 00 00 01 90 00');
assert.equal(r.sw, 0x9000); assert.equal(toHex(r.data), '6F 07 84 05 A0 00 00 00 01');
assert.deepEqual([parseResponse('6A82').sw1, parseResponse('6A82').sw2, parseResponse('6A82').data.length], [0x6a, 0x82, 0]);
assert.throws(() => parseResponse('90'), ApduError);
});
test('invalid input is rejected', () => {
assert.throws(() => encodeCommand({ cla: 256, ins: 0, p1: 0, p2: 0 }), ApduError);
assert.throws(() => encodeCommand({ cla: 0, ins: 0, p1: 0, p2: 0, ne: 65537 }), ApduError);
assert.throws(() => encodeCommand({ cla: 0, ins: 0, p1: 0, p2: 0, data: new Uint8Array(65536) }), ApduError);
assert.throws(() => fromHex('0G'), ApduError);
assert.throws(() => fromHex('ABC'), ApduError);
assert.throws(() => decodeCommand('00A404'), ApduError);
assert.throws(() => decodeCommand('00A40400050102'), ApduError); // Lc says 5, only 2 follow
assert.throws(() => decodeCommand('00A4040000000001'), ApduError); // extended Lc 0000 followed by data
assert.equal(decodeCommand('00A40400000000').ne, 65536); // but 00 0000 alone is case 2 extended, Le = 65536
});
Frequently asked questions
What does APDU stand for?
Application protocol data unit: the unit of data exchanged between a card and the device talking to it, at the level of commands and responses rather than of the transmission protocol underneath.
What is the difference between Le and Ne?
Ne is the number of response bytes you expect; Le is the field that encodes it. In short form an Le of 00 encodes Ne = 256; in extended form 00 00 encodes 65536. An absent Le means no response data is expected.
Why does my card answer 6A 82 to SELECT?
The card has no application matching that AID. When you probe a list of AIDs this is the expected answer for every AID the card does not carry.
Is 90 00 the only success code?
It is the normal one. Some applications define additional success or warning codes of their own; check the application’s specification, and handle 61 XX if your reader stack passes it through.
Can I send extended-length APDUs over NFC on Android?
Only if the card and the phone’s NFC stack support them; IsoDep.isExtendedLengthApduSupported() tells you, and IsoDep.getMaxTransceiveLength() gives the largest APDU you can send.
Related reading on Ambimat
- How to parse BER-TLV data in JavaScript – decoding the data field of a response.
- Big endian vs little endian – byte order in length fields and card data.
- Application discovery and selection on chip cards and implementing application selection.
- Application identifier (AID) list – AIDs to use with SELECT.
- Turning smart-card reader output into host-friendly text – logging APDU exchanges without losing bytes.
Sources
- ETSI TS 102 221 V18.4.0, Smart Cards; UICC-Terminal interface; Physical and logical characteristics – §10.1–10.2 APDU structure, command cases, coding of CLA, INS and status words; §11.1 SELECT, READ BINARY, UPDATE BINARY, READ RECORD; §7.3.1.1.5 procedure bytes: etsi.org
- NIST SP 800-73pt2-5, Interfaces for Personal Identity Verification: Part 2 – PIV Card Application Card Command Interface (2024) – SELECT, GET DATA, VERIFY, command chaining, status words: nvlpubs.nist.gov; Part 1 (PIV AID): nvlpubs.nist.gov
- Oracle, Java SE 21 API,
javax.smartcardio: CommandAPDU, ResponseAPDU, CardChannel; and the OpenJDK source of CommandAPDU (extended-length byte layouts) - Oracle, Java Card API 3.2: APDU, ISO7816, ISOException, ExtendedLength
- Android Developers,
android.nfc.tech.IsoDep(extended length support, maximum transceive length): developer.android.com - S. J. Murdoch, S. Drimer, R. Anderson, M. Bond, Chip and PIN is Broken, IEEE Symposium on Security and Privacy 2010 (published payment-card APDU trace): cl.cam.ac.uk
Written and tested by the Ambimat engineering team. The JavaScript module and examples were run on Node.js 25.8.2; the Java cross-check on OpenJDK 21.