Skip to content

Commit 9856086

Browse files
committed
Add export file parser, CAP standard naming, and Maven build profiles
1 parent 95e07dd commit 9856086

File tree

18 files changed

+823
-184
lines changed

18 files changed

+823
-184
lines changed

capfile/src/main/java/pro/javacard/capfile/CAPFile.java

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ public final class CAPFile {
5959
// Parsed content
6060
private final Map<AID, String> applets = new LinkedHashMap<>();
6161
private final List<CAPPackage> imports = new ArrayList<>();
62-
private CAPPackage pkg;
63-
private byte flags;
64-
private String cap_version;
62+
private final CAPPackage pkg;
63+
private final byte flags;
64+
private final String cap_version;
6565
// Metadata
6666
private Manifest manifest = null; // From 2.2.2
6767
private Document appletxml = null; // From 3.0.1
@@ -172,8 +172,9 @@ protected CAPFile(InputStream in) throws IOException {
172172
AID appaid = new AID(applet, offset, len);
173173
// We might already have it, with the name from metadata
174174
// FIXME: use metadata only as additional source
175-
if (!applets.containsKey(appaid))
175+
if (!applets.containsKey(appaid)) {
176176
applets.put(appaid, null);
177+
}
177178
// Skip install_method_offset
178179
offset += len + 2;
179180
}
@@ -198,8 +199,9 @@ protected CAPFile(InputStream in) throws IOException {
198199
String name = app.getElementsByTagName("applet-class").item(0).getTextContent();
199200
String aidstring = app.getElementsByTagName("applet-AID").item(0).getTextContent();
200201
AID aid = AID.fromString(aidstring.replace("//aid/", "").replace("/", ""));
201-
if (!applets.containsKey(aid))
202+
if (!applets.containsKey(aid)) {
202203
throw new IOException("applet.xml contains missing applet " + aid);
204+
}
203205
applets.put(aid, name);
204206
}
205207
}
@@ -252,10 +254,12 @@ byte[] _getCode(boolean includeDebug) {
252254
ByteArrayOutputStream result = new ByteArrayOutputStream();
253255
for (String name : componentNames) {
254256
byte[] c = getComponent(name);
255-
if (c == null)
257+
if (c == null) {
256258
continue;
257-
if (!includeDebug && (name.equals("Debug") || name.equals("Descriptor")))
259+
}
260+
if (!includeDebug && (name.equals("Debug") || name.equals("Descriptor"))) {
258261
continue;
262+
}
259263
try {
260264
result.write(c);
261265
} catch (IOException e) {
@@ -287,7 +291,7 @@ public void dump(PrintStream out) {
287291
Optional<String> jcv = guessJavaCardVersion();
288292
String gpversion = gpv.isPresent() ? "/GlobalPlatform " + gpv.get() : "";
289293

290-
out.println("CAP file (v" + cap_version + "), contains: " + String.join(", ", getFlags()) + " for JavaCard " + jcv.orElse("2.1.1?") + gpversion);
294+
out.println(String.format("CAP file (v%s), contains: %s for JavaCard %s%s", cap_version, String.join(", ", getFlags()), jcv.orElse("2.1.1?"), gpversion));
291295
out.printf("Package: %s %s v%s%n", pkg.getName().get(), pkg.getAid().toString(), pkg.getVersionString());
292296
for (Map.Entry<AID, String> applet : getApplets().entrySet()) {
293297
out.println("Applet: " + (applet.getValue() == null ? "" : applet.getValue() + " ") + applet.getKey());
@@ -313,11 +317,11 @@ public void dump(PrintStream out) {
313317
String converter_version = caps.getValue("Java-Card-Converter-Version");
314318
String converter_provider = caps.getValue("Java-Card-Converter-Provider");
315319

316-
out.println("Generated by " + converter_provider + " converter " + converter_version);
317-
out.println("On " + cap_creation_time + " with JDK " + jdk_name);
320+
out.println(String.format("Generated by %s converter %s", converter_provider, converter_version));
321+
out.println(String.format("On %s with JDK %s", cap_creation_time, jdk_name));
318322
}
319323
}
320-
out.println("Code size " + getCode().length + " bytes (" + getCode(true).length + " with debug)");
324+
out.println(String.format("Code size %d bytes (%d with debug)", getCode().length, getCode(true).length));
321325
out.println("SHA-256 " + HexUtils.bin2hex(getLoadFileDataHash("SHA-256")).toLowerCase());
322326
out.println("SHA-1 " + HexUtils.bin2hex(getLoadFileDataHash("SHA-1")).toLowerCase());
323327
}
@@ -349,16 +353,23 @@ public Map<AID, String> getApplets() {
349353
return Collections.unmodifiableMap(applets);
350354
}
351355

352-
// Guess the targeted JavaCard version based on javacard.framework version
353-
// See https://stackoverflow.com/questions/25031338/how-to-get-javacard-version-on-card for a nice list
356+
// Guess the targeted JavaCard version based on imported package versions.
357+
//
358+
// Mapping derived from parsing export files in actual SDK kits (jc211 through jc320v25.1):
359+
// framework 1.0=2.1.x, 1.2=2.2.1, 1.3=2.2.2, 1.4=3.0.1, 1.5=3.0.4, 1.6=3.0.5, 1.8=3.1.0, 1.9=3.2.0
360+
// security/crypto 1.1=2.1.x, 1.2=2.2.1, 1.3=2.2.2, 1.4=3.0.1, 1.5=3.0.4, 1.6=3.0.5, 1.7=3.1.0, 1.8=3.2.0
361+
// Note: framework minor versions diverge from security/crypto starting from 3.1.0 (framework skips minor=7).
362+
// Note: SDK 2.1.1 and 2.1.2 ship identical module versions - indistinguishable.
354363
public Optional<String> guessJavaCardVersion() {
364+
// Primary: javacard.framework has a unique version progression (skips minor=7)
355365
AID jf = new AID("A0000000620101"); // javacard.framework
356366
for (CAPPackage p : imports) {
357367
if (p.aid.equals(jf)) {
358368
switch (p.minor) {
359369
case 0:
360370
return Optional.of("2.1.1");
361371
case 1:
372+
// No actual SDK ships framework 1.1; kept for historical reasons
362373
return Optional.of("2.1.2");
363374
case 2:
364375
return Optional.of("2.2.1");
@@ -370,6 +381,7 @@ public Optional<String> guessJavaCardVersion() {
370381
return Optional.of("3.0.4");
371382
case 6:
372383
return Optional.of("3.0.5");
384+
// minor=7 not used by any SDK
373385
case 8:
374386
return Optional.of("3.1.0");
375387
case 9:
@@ -380,9 +392,11 @@ public Optional<String> guessJavaCardVersion() {
380392
}
381393
}
382394

395+
// Fallback: javacard.security and javacardx.crypto share identical version progression
383396
AID js = new AID("A0000000620102"); // javacard.security
397+
AID jc = new AID("A0000000620201"); // javacardx.crypto
384398
for (CAPPackage p : imports) {
385-
if (p.aid.equals(js)) {
399+
if (p.aid.equals(js) || p.aid.equals(jc)) {
386400
switch (p.minor) {
387401
case 1:
388402
return Optional.of("2.1.1");
@@ -398,15 +412,17 @@ public Optional<String> guessJavaCardVersion() {
398412
return Optional.of("3.0.5");
399413
case 7:
400414
return Optional.of("3.1.0");
415+
case 8:
416+
return Optional.of("3.2.0");
401417
default:
402418
return Optional.of(String.format("unknown: %d.%d", p.major, p.minor));
403419
}
404420
}
405421
}
406-
// Assume 2.1.1, for the case where javacard.framework nor javacard.security is not included.
407422
return Optional.empty();
408423
}
409424

425+
// Guess GP version from org.globalplatform import version (A00000015100)
410426
public Optional<String> guessGlobalPlatformVersion() {
411427
AID jf = new AID("A00000015100");
412428
for (CAPPackage p : imports) {

capfile/src/main/java/pro/javacard/capfile/HexUtils.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
*/
2222
package pro.javacard.capfile;
2323

24-
class HexUtils {
24+
public class HexUtils {
25+
private HexUtils() {}
26+
2527
// This code has been taken from Apache commons-codec 1.7 (License: Apache 2.0)
2628
private static final char[] UPPER_HEX = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
2729

capfile/src/main/java/pro/javacard/capfile/WellKnownAID.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,9 @@ private WellKnownAID() {}
9393
// Load internal
9494
try (InputStream in = WellKnownAID.class.getResourceAsStream("aid_list.properties")) {
9595
// If run differently, might not have the list
96-
if (in != null)
96+
if (in != null) {
9797
load(in);
98+
}
9899
} catch (IOException e) {
99100
throw new RuntimeException("Can not load builtin list of AID-s: " + e.getMessage(), e);
100101
}
@@ -120,8 +121,9 @@ public static void load(InputStream in) {
120121
}
121122

122123
public static void load(Path p) {
123-
if (!Files.exists(p))
124+
if (!Files.exists(p)) {
124125
return;
126+
}
125127
try (InputStream in = Files.newInputStream(p)) {
126128
load(in);
127129
} catch (IOException e) {

capfile/src/main/java/pro/javacard/sdk/ExportFileHelper.java

Lines changed: 181 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,37 +21,200 @@
2121
*/
2222
package pro.javacard.sdk;
2323

24+
import pro.javacard.capfile.HexUtils;
25+
2426
import java.io.DataInputStream;
2527
import java.io.IOException;
28+
import java.io.InputStream;
2629
import java.nio.file.Files;
2730
import java.nio.file.Path;
28-
import java.util.Optional;
31+
import java.util.HashMap;
32+
import java.util.Map;
2933

34+
// Export file format: JCVM Spec v3.2, Chapter 5 "The Export File Format"
3035
public final class ExportFileHelper {
3136

37+
// JCVM 5.5: "The magic item contains the magic number identifying the ExportFile format; it has the value 0x00FACADE."
38+
static final int MAGIC = 0x00FACADE;
39+
40+
// JCVM 5.6, Table 5-1: Export File Constant Pool Tags
41+
static final int TAG_UTF8 = 1;
42+
static final int TAG_INTEGER = 3;
43+
static final int TAG_CLASSREF = 7;
44+
static final int TAG_PACKAGE = 13;
45+
3246
public enum ExportFileVersion {
33-
V21, V23
47+
V21,
48+
V22,
49+
V23
50+
}
51+
52+
public static final class PackageInfo {
53+
private final ExportFileVersion version;
54+
private final String name;
55+
private final byte[] aid;
56+
private final int major;
57+
private final int minor;
58+
private final boolean library;
59+
60+
PackageInfo(ExportFileVersion version, String name, byte[] aid,
61+
int major, int minor, boolean library) {
62+
this.version = version;
63+
this.name = name;
64+
this.aid = aid.clone();
65+
this.major = major;
66+
this.minor = minor;
67+
this.library = library;
68+
}
69+
70+
public ExportFileVersion getVersion() {
71+
return version;
72+
}
73+
74+
public String getName() {
75+
return name;
76+
}
77+
78+
public byte[] getAid() {
79+
return aid.clone();
80+
}
81+
82+
public int getMajor() {
83+
return major;
84+
}
85+
86+
public int getMinor() {
87+
return minor;
88+
}
89+
90+
public String getPackageVersion() {
91+
return String.format("%d.%d", major, minor);
92+
}
93+
94+
public boolean isLibrary() {
95+
return library;
96+
}
97+
98+
@Override
99+
public String toString() {
100+
return String.format("%s %s%s v%s (%s)", name, HexUtils.bin2hex(aid), library ? " library" : "", getPackageVersion(), version);
101+
}
34102
}
35103

36104
private ExportFileHelper() {
37105
}
38106

39-
public static Optional<ExportFileVersion> getVersion(Path path) throws IOException {
40-
try (DataInputStream dis = new DataInputStream(Files.newInputStream(path))) {
41-
int magic = dis.readInt();
42-
43-
byte minor = dis.readByte();
44-
byte major = dis.readByte();
45-
46-
if (magic != 0x00FACADE)
47-
return Optional.empty();
48-
if (major != 2)
49-
throw new IOException("Invalid major version: " + major);
50-
if (minor == 1)
51-
return Optional.of(ExportFileVersion.V21);
52-
if (minor == 3)
53-
return Optional.of(ExportFileVersion.V23);
54-
throw new IOException("Invalid minor version: " + minor);
107+
public static PackageInfo parsePackage(Path path) throws IOException {
108+
try (InputStream in = Files.newInputStream(path)) {
109+
return parsePackage(in);
110+
}
111+
}
112+
113+
public static PackageInfo parsePackage(InputStream in) throws IOException {
114+
DataInputStream dis = new DataInputStream(in);
115+
116+
// JCVM 5.5: magic (u4)
117+
int magic = dis.readInt();
118+
if (magic != MAGIC) {
119+
throw new IllegalArgumentException(String.format("Bad magic: 0x%08X", magic));
120+
}
121+
122+
// JCVM 5.5: minor_version (u1), major_version (u1)
123+
byte fileMinor = dis.readByte();
124+
byte fileMajor = dis.readByte();
125+
if (fileMajor != 2) {
126+
throw new IllegalArgumentException("Invalid export file major version: " + fileMajor);
127+
}
128+
ExportFileVersion version = parseFileVersion(fileMinor);
129+
130+
// JCVM 5.5: constant_pool_count (u2)
131+
int cpCount = dis.readUnsignedShort();
132+
// JCVM 5.6: constant_pool[]
133+
Object[] pool = new Object[cpCount];
134+
135+
// Store all CONSTANT_Package entries by index, since this_package
136+
// tells us which one is the actual exported package
137+
Map<Integer, int[]> pkgEntries = new HashMap<>(); // index -> [flags, nameIndex, minor, major]
138+
Map<Integer, byte[]> pkgAids = new HashMap<>(); // index -> aid
139+
140+
for (int i = 0; i < cpCount; i++) {
141+
int tag = dis.readUnsignedByte();
142+
switch (tag) {
143+
case TAG_UTF8: {
144+
// JCVM 5.6.4: length (u2), bytes[length]
145+
int len = dis.readUnsignedShort();
146+
byte[] bytes = new byte[len];
147+
dis.readFully(bytes);
148+
pool[i] = new String(bytes, "UTF-8");
149+
break;
150+
}
151+
case TAG_INTEGER: {
152+
// JCVM 5.6.3: bytes (u4)
153+
dis.readInt();
154+
break;
155+
}
156+
case TAG_CLASSREF: {
157+
// JCVM 5.6.2: name_index (u2)
158+
dis.readUnsignedShort();
159+
break;
160+
}
161+
case TAG_PACKAGE: {
162+
// JCVM 5.6.1: flags (u1), name_index (u2),
163+
// minor_version (u1), major_version (u1), aid_length (u1), aid[aid_length]
164+
int flags = dis.readUnsignedByte();
165+
int nameIndex = dis.readUnsignedShort();
166+
int minor = dis.readUnsignedByte();
167+
int major = dis.readUnsignedByte();
168+
int aidLen = dis.readUnsignedByte();
169+
byte[] aid = new byte[aidLen];
170+
dis.readFully(aid);
171+
pkgEntries.put(i, new int[]{flags, nameIndex, minor, major});
172+
pkgAids.put(i, aid);
173+
break;
174+
}
175+
default:
176+
throw new IllegalArgumentException(String.format("Unknown constant pool tag: %d at index %d", tag, i));
177+
}
178+
}
179+
180+
// JCVM 5.5: this_package (u2) - index into constant pool identifying the exported package
181+
int thisPackage = dis.readUnsignedShort();
182+
183+
if (!pkgEntries.containsKey(thisPackage)) {
184+
throw new IllegalArgumentException(String.format("this_package index %d does not point to a CONSTANT_Package", thisPackage));
185+
}
186+
187+
int[] pkg = pkgEntries.get(thisPackage);
188+
int pkgFlags = pkg[0];
189+
int pkgNameIndex = pkg[1];
190+
int pkgMinor = pkg[2];
191+
int pkgMajor = pkg[3];
192+
byte[] pkgAid = pkgAids.get(thisPackage);
193+
194+
if (pkgNameIndex >= cpCount || !(pool[pkgNameIndex] instanceof String)) {
195+
throw new IllegalArgumentException("Invalid package name index: " + pkgNameIndex);
196+
}
197+
198+
// JCVM 5.6.1: name_index -> CONSTANT_Utf8 with fully qualified package name using '/'
199+
String name = ((String) pool[pkgNameIndex]).replace('/', '.');
200+
201+
// JCVM 5.6.1, Table 5-2: "If bit 0 of the flags item is set, this package is a library"
202+
boolean library = (pkgFlags & 0x01) != 0;
203+
204+
return new PackageInfo(version, name, pkgAid, pkgMajor, pkgMinor, library);
205+
}
206+
207+
// JCVM 5.5: "major version has the value 2", minor 1=v2.1, 2=v2.2, 3=v2.3
208+
private static ExportFileVersion parseFileVersion(int minor) {
209+
switch (minor) {
210+
case 1:
211+
return ExportFileVersion.V21;
212+
case 2:
213+
return ExportFileVersion.V22;
214+
case 3:
215+
return ExportFileVersion.V23;
216+
default:
217+
throw new IllegalArgumentException("Invalid export file minor version: " + minor);
55218
}
56219
}
57220
}

0 commit comments

Comments
 (0)