Lazy, composable APDU interaction framework for Java 21+. Describe card interactions as Recipe values, compose
them with monadic operators, execute with a Chef. No I/O happens until a chef runs the recipe.
A Recipe<T> is a pure description of a card interaction that produces a value of type T. Composing recipes does
no I/O - it builds a data structure. Execution happens only when a Chef runs it.
// SELECT applet, read FCI length, use it in a follow-up READ BINARY
var recipe = Cookbook.selectFCI(aid)
.map(fci -> fci.getData().length)
.then(len -> Cookbook.send(new CommandAPDU(0x00, 0xB0, 0x00, 0x00, len)));
var response = chef.cook(recipe);Cookbook provides building blocks for common operations: selectFCI(), send(), data(), uid().
It also provides taster functions (expect(), data(), check(), any(), all()) for evaluating card responses.
then(f) chains a dependent operation on the result (flatMap). map(f) transforms the result without I/O.
and(next) sequences two recipes, discarding the first result:
var recipe = Cookbook.selectFCI(aid)
.and(Cookbook.uid()) // SELECT, then read UID
.map(HexUtils::bin2hex); // format as hex stringRecipes read typed Preferences at prepare-time, and tasters receive them at evaluate-time, so the same
recipe adapts to different card configurations without rewriting. Cookbook.send() defers APDU construction to prepare():
var le = Preference.of("le", Integer.class, 256, false);
var recipe = Cookbook.send(
prefs -> new CommandAPDU(0x00, 0xB0, 0x00, 0x00, prefs.get(le)),
Cookbook.expect(0x9000));orElse() falls back on SW mismatch, recover() inspects the error (including the full ResponseAPDU),
optional() absorbs card errors as Optional.empty():
// Try primary AID, fall back to secondary
var recipe = Cookbook.selectFCI(primaryAid)
.orElse(Cookbook.selectFCI(secondaryAid));
// Try multiple alternatives, first success wins
var recipe = Cookbook.firstOf(List.of(
Cookbook.selectFCI(aid1),
Cookbook.selectFCI(aid2),
Cookbook.selectFCI(aid3)));
// Probe without aborting the chain
var recipe = Cookbook.uid().optional()
.then(opt -> opt.isPresent()
? Recipe.premade("UID: " + HexUtils.bin2hex(opt.get()))
: Recipe.premade("no UID support"));Errors are scoped to the current step - downstream then() continuations are not affected by upstream
orElse/recover handlers.
Send multiple commands in a single step and evaluate all responses together:
var cmds = List.of(
new CommandAPDU(0x80, 0xE8, 0x00, 0x00, chunk1),
new CommandAPDU(0x80, 0xE8, 0x80, 0x01, chunk2));
var recipe = Cookbook.send(cmds, 0x9000); // checks all responsesSousChef is the real-time executor. It transmits APDUs over a BIBO transport using a trampoline loop -
each prepare() call produces commands to send, responses come back, the taster examines them alongside the current preferences and decides what happens next
(Ready, NextStep, or Error):
var chef = new SousChef(bibo);
var result = chef.cook(recipe);
// Or with preferences and accumulated metadata:
var dish = chef.serve(recipe, prefs);
var sessionId = dish.preferences().valueOf(SESSION_ID).orElseThrow();MiseEnPlaceChef is the pre-computation executor. It runs recipes using declared expected responses instead
of real card I/O - useful for pre-validating recipe structure or generating APDU sequences without hardware.
Recipe<T> prepares into one of three PreparationStep variants:
Premade<T>- pure value, no I/O neededIngredients<T>- commands to send + a taster function that evaluates responsesFailed<T>- known failure at prepare-time (fromRecipe.error())
The taster receives the card responses and the current Preferences, then returns a Verdict:
Ready<T>- done, here's the resultNextStep<T>- continue with a new recipe (optionally enriching preferences)Error<T>- card error, carries theResponseAPDUfor diagnostics
Unhandled errors throw KitchenDisaster (extends RuntimeException).
Requires Java 21. Depends on apdu4j-core and apdu4j-prefs:
<dependency>
<groupId>com.github.martinpaljak</groupId>
<artifactId>apdu4j-apdulette</artifactId>
<version>LATEST</version>
</dependency>