From 9a9b5137c8ee5977e1d4bf4d2c2f2e0c833caa80 Mon Sep 17 00:00:00 2001 From: Martin Paljak Date: Thu, 29 Feb 2024 15:16:32 +0200 Subject: [PATCH] Get rid of inner classes and some related shenanigans --- README.md | 5 + capfile/pom.xml | 8 - .../java/pro/javacard/capfile/CAPFile.java | 30 +- .../java/pro/javacard/capfile/CAPPackage.java | 2 +- .../pro/javacard/sdk/OffCardVerifier.java | 8 +- pom.xml | 18 + spotbugs.xml | 3 + .../main/java/pro/javacard/ant/DummyMain.java | 7 +- .../javacard/ant/HelpingBuildException.java | 11 + .../main/java/pro/javacard/ant/JCApplet.java | 27 + .../src/main/java/pro/javacard/ant/JCCap.java | 747 +++++++++++++++ .../main/java/pro/javacard/ant/JCImport.java | 15 + .../main/java/pro/javacard/ant/JavaCard.java | 894 +----------------- task/src/main/java/pro/javacard/ant/Misc.java | 109 +++ 14 files changed, 970 insertions(+), 914 deletions(-) create mode 100644 spotbugs.xml create mode 100644 task/src/main/java/pro/javacard/ant/HelpingBuildException.java create mode 100644 task/src/main/java/pro/javacard/ant/JCApplet.java create mode 100644 task/src/main/java/pro/javacard/ant/JCCap.java create mode 100644 task/src/main/java/pro/javacard/ant/JCImport.java create mode 100644 task/src/main/java/pro/javacard/ant/Misc.java diff --git a/README.md b/README.md index f04cb26..2c14410 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,11 @@ Notes: - run off-card verifier - `java -jar ant-javacard.jar ` +### Environment variables +- `JAVA_HOME` - path to the JDK to be used. +- `JC_HOME` - path to the JavaCard SDK to be used if not specified in the build file. +- `ANT_JAVACARD_TMP` - path to the temporary folder to be used for building CAP files. This is not cleaned after use. +- `DEBUG` - if set, shows debug output. ## Maven dependency Releases are published to [`https://mvn.javacard.pro/maven/`](https://mvn.javacard.pro/maven/). To use it, add this to your `pom.xml`: diff --git a/capfile/pom.xml b/capfile/pom.xml index 2c12a7d..ef4049b 100644 --- a/capfile/pom.xml +++ b/capfile/pom.xml @@ -76,14 +76,6 @@ - - com.github.spotbugs - spotbugs-maven-plugin - 4.8.3.1 - - spotbugs.xml - - \ No newline at end of file diff --git a/capfile/src/main/java/pro/javacard/capfile/CAPFile.java b/capfile/src/main/java/pro/javacard/capfile/CAPFile.java index 2c7217b..18a9046 100644 --- a/capfile/src/main/java/pro/javacard/capfile/CAPFile.java +++ b/capfile/src/main/java/pro/javacard/capfile/CAPFile.java @@ -53,10 +53,6 @@ * CAP files are tiny, so we keep it in memory. */ public final class CAPFile { - public static final String DAP_RSA_V1_SHA1_FILE = "dap.rsa.sha1"; - public static final String DAP_RSA_V1_SHA256_FILE = "dap.rsa.sha256"; - public static final String DAP_P256_SHA1_FILE = "dap.p256.sha1"; - public static final String DAP_P256_SHA256_FILE = "dap.p256.sha256"; private static final String[] componentNames = {"Header", "Directory", "Import", "Applet", "Class", "Method", "StaticField", "Export", "ConstantPool", "RefLocation", "Descriptor", "Debug"}; @@ -66,7 +62,7 @@ public final class CAPFile { private final List imports = new ArrayList<>(); private CAPPackage pkg; private byte flags; - private String cap_version; // 2.1 and 2.2 supported, 2.3 new format not + private String cap_version; // Metadata private Manifest manifest = null; // From 2.2.2 private Document appletxml = null; // From 3.0.1 @@ -102,6 +98,10 @@ public byte[] getMetaInfEntry(String name) { return entries.get("META-INF/" + name); } + public Optional getZipComponent(String name) { + return Optional.ofNullable(entries.get(name)); + } + public void store(OutputStream to) throws IOException { try (ZipOutputStream out = new ZipOutputStream(to)) { for (Map.Entry e : entries.entrySet()) { @@ -246,16 +246,20 @@ public byte[] getCode(boolean includeDebug) { } byte[] _getCode(boolean includeDebug) { - byte[] result = new byte[0]; + ByteArrayOutputStream result = new ByteArrayOutputStream(); for (String name : componentNames) { byte[] c = getComponent(name); if (c == null) continue; if (!includeDebug && (name.equals("Debug") || name.equals("Descriptor"))) continue; - result = concat(result, c); + try { + result.write(c); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } - return result; + return result.toByteArray(); } public byte[] getLoadFileDataHash(String hash) { @@ -316,6 +320,10 @@ public void dump(PrintStream out) { } public List getFlags() { + return flags2strings(flags); + } + + public static List flags2strings(byte flags) { ArrayList result = new ArrayList<>(); // Table 6-3: CAP File Package Flags if ((flags & 0x01) == 0x01) { @@ -425,12 +433,6 @@ private static String jcdir2pkg(String jcdir) { return jcdir.substring(0, jcdir.lastIndexOf("/javacard/")).replace('/', '.'); } - private static byte[] concat(byte[] a, byte[] b) { - byte[] r = Arrays.copyOf(a, a.length + b.length); - System.arraycopy(b, 0, r, a.length, b.length); - return r; - } - public static void uncheckedDelete(Path p) throws UncheckedIOException { try { Files.delete(p); diff --git a/capfile/src/main/java/pro/javacard/capfile/CAPPackage.java b/capfile/src/main/java/pro/javacard/capfile/CAPPackage.java index f642168..66daf6f 100644 --- a/capfile/src/main/java/pro/javacard/capfile/CAPPackage.java +++ b/capfile/src/main/java/pro/javacard/capfile/CAPPackage.java @@ -26,8 +26,8 @@ public final class CAPPackage { final AID aid; - final int minor; final int major; + final int minor; final String name; public CAPPackage(AID aid, int major, int minor) { diff --git a/capfile/src/main/java/pro/javacard/sdk/OffCardVerifier.java b/capfile/src/main/java/pro/javacard/sdk/OffCardVerifier.java index 331df13..2e73092 100644 --- a/capfile/src/main/java/pro/javacard/sdk/OffCardVerifier.java +++ b/capfile/src/main/java/pro/javacard/sdk/OffCardVerifier.java @@ -34,12 +34,14 @@ import java.util.jar.JarFile; import java.util.stream.Collectors; +import static pro.javacard.sdk.SDKVersion.*; + public final class OffCardVerifier { private final JavaCardSDK sdk; public static OffCardVerifier withSDK(JavaCardSDK sdk) { // Only main method in 2.1 SDK - if (sdk.getVersion().isOneOf(SDKVersion.V211, SDKVersion.V212)) + if (sdk.getVersion().isOneOf(V211, V212)) throw new RuntimeException("Verification is supported with JavaCard SDK 2.2.1 or later"); return new OffCardVerifier(sdk); } @@ -57,7 +59,7 @@ public void verifyAgainst(File f, JavaCardSDK target, Vector exps) throws public void verifyAgainst(Path f, JavaCardSDK target, List exps) throws VerifierError, IOException { // Warn about recommended usage - if (target.getVersion().isOneOf(SDKVersion.V304, SDKVersion.V305, SDKVersion.V310) && sdk.getVersion() != SDKVersion.V320) { + if (target.getVersion().isOneOf(V304, V305, V310) && sdk.getVersion() != V320) { System.err.println("NB! Please use JavaCard SDK 3.2.0 for verifying!"); } else { if (!sdk.getRelease().equals("3.0.5u3")) { @@ -97,7 +99,7 @@ public void verify(Path f, List exps) throws VerifierError, IOException { try (FileInputStream input = new FileInputStream(f.toFile())) { // 3.0.5u1 still uses old signature - if (sdk.getRelease().equals("3.0.5u3") || sdk.getRelease().equals("3.0.5u2") || sdk.getVersion().isOneOf(SDKVersion.V310, SDKVersion.V320)) { + if (sdk.getRelease().equals("3.0.5u3") || sdk.getRelease().equals("3.0.5u2") || sdk.getVersion().isOneOf(V310, V320)) { Method m = verifier.getMethod("verifyCap", File.class, String.class, Vector.class); m.invoke(null, f.toFile(), packagename, expfiles); } else { diff --git a/pom.xml b/pom.xml index d215b15..7d77f55 100644 --- a/pom.xml +++ b/pom.xml @@ -49,6 +49,24 @@ 3.5.3 + + + com.github.spotbugs + spotbugs-maven-plugin + 4.8.3.1 + + spotbugs.xml + + + + verify + + check + + + + + diff --git a/spotbugs.xml b/spotbugs.xml new file mode 100644 index 0000000..5e55494 --- /dev/null +++ b/spotbugs.xml @@ -0,0 +1,3 @@ + + + diff --git a/task/src/main/java/pro/javacard/ant/DummyMain.java b/task/src/main/java/pro/javacard/ant/DummyMain.java index 7feea41..1dcc0f5 100644 --- a/task/src/main/java/pro/javacard/ant/DummyMain.java +++ b/task/src/main/java/pro/javacard/ant/DummyMain.java @@ -39,12 +39,13 @@ public final class DummyMain { public static void main(String[] argv) { try { Vector args = new Vector<>(Arrays.asList(argv)); - System.out.println("args: " + args); + if (args.isEmpty()) { - System.out.println("This is an ANT task, not a program!"); + System.out.println("This is an ANT task."); System.out.println("Read usage instructions from https://github.com/martinpaljak/ant-javacard#syntax"); System.out.println(); System.out.println("But you can use it to dump/verify CAP files, like this:"); + System.out.println("$ java -jar ant-javacard.jar "); System.exit(1); } else if (args.size() == 1) { final String capfile = args.remove(0); @@ -87,7 +88,7 @@ public static void main(String[] argv) { System.exit(1); } } - } catch (Exception e) { + } catch (Throwable e) { System.err.printf("Error: %s: %s%n", e.getClass().getSimpleName(), e.getMessage()); if (System.getenv("DEBUG") != null) { e.printStackTrace(); diff --git a/task/src/main/java/pro/javacard/ant/HelpingBuildException.java b/task/src/main/java/pro/javacard/ant/HelpingBuildException.java new file mode 100644 index 0000000..48ca3e4 --- /dev/null +++ b/task/src/main/java/pro/javacard/ant/HelpingBuildException.java @@ -0,0 +1,11 @@ +package pro.javacard.ant; + +import org.apache.tools.ant.BuildException; + +public class HelpingBuildException extends BuildException { + private final static long serialVersionUID = -2365126253968479314L; + + public HelpingBuildException(String msg) { + super(msg + "\n\nPLEASE READ https://github.com/martinpaljak/ant-javacard#readme"); + } +} diff --git a/task/src/main/java/pro/javacard/ant/JCApplet.java b/task/src/main/java/pro/javacard/ant/JCApplet.java new file mode 100644 index 0000000..9b8901c --- /dev/null +++ b/task/src/main/java/pro/javacard/ant/JCApplet.java @@ -0,0 +1,27 @@ +package pro.javacard.ant; + +import org.apache.tools.ant.BuildException; + +// Just for Ant +public class JCApplet { + String klass = null; + byte[] aid = null; + + public JCApplet() { + } + + public void setClass(String msg) { + klass = msg; + } + + public void setAID(String msg) { + try { + aid = Misc.stringToBin(msg); + if (aid.length < 5 || aid.length > 16) { + throw new BuildException("Applet AID must be between 5 and 16 bytes: " + aid.length); + } + } catch (IllegalArgumentException e) { + throw new BuildException("Not a valid applet AID: " + e.getMessage()); + } + } +} diff --git a/task/src/main/java/pro/javacard/ant/JCCap.java b/task/src/main/java/pro/javacard/ant/JCCap.java new file mode 100644 index 0000000..5ff31ca --- /dev/null +++ b/task/src/main/java/pro/javacard/ant/JCCap.java @@ -0,0 +1,747 @@ +package pro.javacard.ant; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.Task; +import org.apache.tools.ant.taskdefs.Jar; +import org.apache.tools.ant.taskdefs.Java; +import org.apache.tools.ant.taskdefs.Javac; +import org.apache.tools.ant.types.Environment; +import org.apache.tools.ant.types.FileSet; +import pro.javacard.capfile.CAPFile; +import pro.javacard.sdk.JavaCardSDK; +import pro.javacard.sdk.OffCardVerifier; +import pro.javacard.sdk.SDKVersion; +import pro.javacard.sdk.VerifierError; + +import java.io.File; +import java.io.IOException; +import java.nio.file.*; +import java.util.*; +import java.util.regex.Pattern; + +import static pro.javacard.sdk.SDKVersion.*; + +public class JCCap extends Task { + private final String master_jckit_path; + private JavaCardSDK jckit = null; + private String classes_path = null; + private String sources_path = null; + private String sources2_path = null; + private String includes = null; + private String excludes = null; + private String package_name = null; + private byte[] package_aid = null; + private String package_version = null; + private Vector raw_applets = new Vector<>(); + private Vector raw_imports = new Vector<>(); + private String output_cap = null; + private String output_exp = null; + private String output_jar = null; + private String output_jca = null; + private String jckit_path = null; + private JavaCardSDK targetsdk = null; + private String raw_targetsdk = null; + + private boolean verify = true; + private boolean debug = false; + private boolean strip = false; + private boolean ints = false; + + + public JCCap(String master_jckit_path) { + this.master_jckit_path = master_jckit_path; + } + + public void setJCKit(String msg) { + jckit_path = msg; + } + + public void setOutput(String msg) { + output_cap = msg; + } + + public void setExport(String msg) { + output_exp = msg; + } + + public void setJar(String msg) { + output_jar = msg; + } + + public void setJca(String msg) { + output_jca = msg; + } + + public void setPackage(String msg) { + package_name = msg; + } + + public void setClasses(String msg) { + classes_path = msg; + } + + public void setVersion(String msg) { + package_version = msg; + } + + public void setSources(String arg) { + sources_path = arg; + } + + public void setSources2(String arg) { + sources2_path = arg; + } + + public void setIncludes(String arg) { + includes = arg; + } + + public void setExcludes(String arg) { + excludes = arg; + } + + public void setVerify(boolean arg) { + verify = arg; + } + + public void setDebug(boolean arg) { + debug = arg; + } + + public void setStrip(boolean arg) { + strip = arg; + } + + public void setInts(boolean arg) { + ints = arg; + } + + public void setTargetsdk(String arg) { + raw_targetsdk = arg; + } + + public void setAID(String msg) { + try { + package_aid = Misc.stringToBin(msg); + if (package_aid.length < 5 || package_aid.length > 16) + throw new BuildException("Package AID must be between 5 and 16 bytes: " + Misc.encodeHexString(package_aid) + " (" + package_aid.length + ")"); + + } catch (IllegalArgumentException e) { + throw new BuildException("Not a correct package AID: " + e.getMessage()); + } + } + + // Many applets inside one package + public JCApplet createApplet() { + JCApplet applet = new JCApplet(); + raw_applets.add(applet); + return applet; + } + + // Many imports inside one package + public JCImport createImport() { + JCImport imp = new JCImport(); + raw_imports.add(imp); + return imp; + } + + // To support usage from Gradle, where import is a reserved name + public JCImport createJimport() { + return this.createImport(); + } + + private Optional findSDK() { + // try local configuration first + if (jckit_path != null) { + return JavaCardSDK.detectSDK(getProject().resolveFile(jckit_path).toPath()); + } + // then try the master configuration + if (master_jckit_path != null) { + return JavaCardSDK.detectSDK(getProject().resolveFile(master_jckit_path).toPath()); + } + // now check via ant property + String propPath = getProject().getProperty("jc.home"); + if (propPath != null) { + return JavaCardSDK.detectSDK(getProject().resolveFile(propPath).toPath()); + } + // finally via the environment + String envPath = System.getenv("JC_HOME"); + if (envPath != null) { + return JavaCardSDK.detectSDK(getProject().resolveFile(envPath).toPath()); + } + // return null if no options + return Optional.empty(); + } + + // Check that arguments are sufficient and do some DWIM + private void check() { + jckit = findSDK().orElseThrow(() -> new HelpingBuildException("No usable JavaCard SDK referenced")); + + log("INFO: using JavaCard " + jckit.getVersion() + " SDK in " + jckit.getRoot() + " with JDK " + Misc.getCurrentJDKVersion(), Project.MSG_INFO); + + if (raw_targetsdk != null) { + Optional targetVersion = SDKVersion.fromVersion(raw_targetsdk); + if (jckit != null && jckit.getVersion().isOneOf(V310, V320) && targetVersion.isPresent()) { + SDKVersion target = targetVersion.get(); + // FIXME: can't target 3.2.0 with 3.1.0 + if (target.isOneOf(V304, V305, V310, V320)) { + targetsdk = jckit.target(target); + } else { + throw new HelpingBuildException("Can not target JavaCard " + target + " with JavaCard kit " + jckit.getVersion()); + } + } else { + targetsdk = JavaCardSDK.detectSDK(getProject().resolveFile(raw_targetsdk).toPath()).orElseThrow(() -> new HelpingBuildException("Invalid targetsdk: " + raw_targetsdk)); + if (jckit.getVersion() == V310 && !targetsdk.getVersion().isOneOf(V304, V305, V310)) { + throw new HelpingBuildException("targetsdk " + targetsdk.getVersion() + " is not compatible with jckit " + jckit.getVersion()); + } + } + } + + if (targetsdk == null) { + targetsdk = jckit; + } else { + log("INFO: targeting JavaCard " + targetsdk.getVersion() + " SDK in " + targetsdk.getRoot(), Project.MSG_INFO); + } + + // Warn about deprecation in future + if (sources_path != null && sources2_path != null) { + log("WARN: sources2 is deprecated in favor of multiple paths in sources", Project.MSG_WARN); + } + + // Shorthand for simple small projects - use Maven conventions + if (sources_path == null && classes_path == null) { + if (getProject().resolveFile("src/main/javacard").isDirectory()) + sources_path = "src/main/javacard"; + else if (getProject().resolveFile("src/main/java").isDirectory()) + sources_path = "src/main/java"; + } + + // sources or classes must be set + if (sources_path == null && classes_path == null) { + throw new HelpingBuildException("Must specify sources or classes"); + } + + // Check package version + if (package_version == null) { + package_version = "0.0"; + } else { + // Allowed values are 0..127 + if (!package_version.matches("^[0-9]{1,3}\\.[0-9]{1,3}$")) { + throw new HelpingBuildException("Invalid package version: " + package_version); + } + Arrays.asList(package_version.split("\\.")).stream().map(e -> Integer.parseInt(e, 10)).forEach(e -> { + if (e < 0 || e > 127) + throw new HelpingBuildException("Illegal package version value: " + package_version); + }); + } + + // Check imports + for (JCImport a : raw_imports) { + if (a.jar != null && !getProject().resolveFile(a.jar).isFile()) + throw new BuildException("Import JAR does not exist: " + a.jar); + if (a.exps != null && !getProject().resolveFile(a.exps).isDirectory()) + throw new BuildException("Import EXP files folder does not exist: " + a.exps); + } + // Construct applets and fill in missing bits from package info, if necessary + int applet_counter = 0; + for (JCApplet a : raw_applets) { + // Keep count for automagic numbering + applet_counter = applet_counter + 1; + + if (a.klass == null) { + throw new HelpingBuildException("Applet class is missing"); + } + // If package name is present, must match the applet + if (package_name != null) { + if (!a.klass.contains(".")) { + a.klass = package_name + "." + a.klass; + } else if (!a.klass.startsWith(package_name)) { + throw new HelpingBuildException("Applet class " + a.klass + " is not in package " + package_name); + } + } else { + if (a.klass.contains(".")) { + String pkgname = a.klass.substring(0, a.klass.lastIndexOf(".")); + log("INFO: Setting package name to " + pkgname, Project.MSG_INFO); + package_name = pkgname; + } else { + throw new HelpingBuildException("Applet must be in a package!"); + } + } + + // If applet AID is present, must match the package AID + if (package_aid != null) { + if (a.aid != null) { + // RID-s must match + if (!Arrays.equals(Arrays.copyOf(package_aid, 5), Arrays.copyOf(a.aid, 5))) { + throw new HelpingBuildException("Package RID does not match Applet RID"); + } + } else { + // make "magic" applet AID from package_aid + counter + a.aid = Arrays.copyOf(package_aid, package_aid.length + 1); + a.aid[package_aid.length] = (byte) applet_counter; + log("INFO: generated applet AID: " + Misc.encodeHexString(a.aid) + " for " + a.klass, Project.MSG_INFO); + } + } else { + // if package AID is empty, just set it to the minimal from + // applet + if (a.aid != null) { + package_aid = Arrays.copyOf(a.aid, 5); + } else { + throw new HelpingBuildException("Both package AID and applet AID are missing!"); + } + } + } + + // Check package AID + if (package_aid == null) { + throw new HelpingBuildException("Must specify package AID"); + } + + // Package name must be present if no applets + if (raw_applets.size() == 0) { + if (package_name == null) + throw new HelpingBuildException("Must specify package name if no applets"); + log("Building library from package " + package_name + " (AID: " + Misc.encodeHexString(package_aid) + ")", Project.MSG_INFO); + } else { + log("Building CAP with " + applet_counter + " applet" + (applet_counter > 1 ? "s" : "") + " from package " + package_name + " (AID: " + Misc.encodeHexString(package_aid) + ")", Project.MSG_INFO); + for (JCApplet app : raw_applets) { + log(app.klass + " " + Misc.encodeHexString(app.aid), Project.MSG_INFO); + } + } + if (output_exp != null) { + // Last component of the package + String ln = package_name; + if (ln.lastIndexOf(".") != -1) { + ln = ln.substring(ln.lastIndexOf(".") + 1); + } + output_jar = new File(output_exp, ln + ".jar").toString(); + } + // Default output name + if (output_cap == null) { + output_cap = "%n_%a_%h_%j.cap"; // SomeApplet_010203040506_9a037e30_2.2.2.cap + } + } + + // To lessen the java.nio and apache.ant namespace clash... + private org.apache.tools.ant.types.Path mkPath(String name) { + if (name == null) + return new org.apache.tools.ant.types.Path(getProject()); + return new org.apache.tools.ant.types.Path(getProject(), name); + } + + private void compile() { + Project project = getProject(); + setTaskName("compile"); + + // construct javac task + Javac j = new Javac(); + j.setProject(project); + j.setTaskName("compile"); + + org.apache.tools.ant.types.Path sources = mkPath(null); + + // New style - multiple folders + String pattern = Pattern.quote(File.pathSeparator); + String[] sources_paths = sources_path.split(pattern); + for (String path : sources_paths) + sources.append(mkPath(path)); + + // Old style - second folder + if (sources2_path != null) + sources.append(mkPath(sources2_path)); + j.setSrcdir(sources); + + if (includes != null) { + j.setIncludes(includes); + } + + if (excludes != null) { + j.setExcludes(excludes); + } + + // We resolve files to compile based on the sources/includes/excludes parameters, so don't set sourcepath + j.setSourcepath(new org.apache.tools.ant.types.Path(project, null)); + + log("Compiling files from " + sources, Project.MSG_INFO); + + // determine output directory + Path tmp; + if (classes_path != null) { + // if specified use that + tmp = project.resolveFile(classes_path).toPath(); + if (!Files.exists(tmp)) { + try { + Files.createDirectories(tmp); + } catch (IOException e) { + throw new BuildException("Could not create classes folder " + tmp.toAbsolutePath()); + } + } + } else { + // else generate temporary folder + tmp = makeTemp("classes"); + classes_path = tmp.toAbsolutePath().toString(); + } + + j.setDestdir(tmp.toFile()); + // See "Setting Java Compiler Options" in User Guide + j.setDebug(true); + j.setDebugLevel("lines,vars,source"); + + // set the best option supported by jckit + String javaVersion = JavaCardSDK.getJavaVersion(jckit.getVersion()); + // Warn in human readable way if Java not compatible with JC Kit + // See https://github.com/martinpaljak/ant-javacard/issues/79 + int jdkver = Misc.getCurrentJDKVersion(); + if (jdkver > 17) { + throw new HelpingBuildException("JDK 17 is the latest supported JDK."); + } else if (jckit.getVersion().isOneOf(V211, V212, V221, V222) && jdkver > 8) { + throw new HelpingBuildException("Use JDK 8 with JavaCard kit v2.x"); + } else if (jdkver > 11 && !jckit.getVersion().isOneOf(V310, V320)) { + throw new HelpingBuildException("Use JDK 11 with JavaCard kit " + jckit.getVersion()); + } + + j.setTarget(javaVersion); + j.setSource(javaVersion); + + j.setIncludeantruntime(false); + j.createCompilerArg().setValue("-Xlint"); + j.createCompilerArg().setValue("-Xlint:-options"); + j.createCompilerArg().setValue("-Xlint:-serial"); + if (jckit.getVersion().isOneOf(V304, V305, V310)) { + //-processor com.oracle.javacard.stringproc.StringConstantsProcessor \ + // -processorpath "JCDK_HOME/lib/tools.jar;JCDK_HOME/lib/api_classic_annotations.jar" \ + j.createCompilerArg().setLine("-processor com.oracle.javacard.stringproc.StringConstantsProcessor"); + org.apache.tools.ant.types.Path pcp = new Javac().createClasspath(); + for (Path jar : jckit.getCompilerJars()) { + pcp.append(mkPath(jar.toString())); + } + j.createCompilerArg().setLine("-processorpath \"" + pcp.toString() + "\""); + j.createCompilerArg().setValue("-Xlint:all,-processing"); + } + + j.setFailonerror(true); + j.setFork(true); + j.setListfiles(true); + + // set classpath + org.apache.tools.ant.types.Path cp = j.createClasspath(); + JavaCardSDK sdk = targetsdk == null ? jckit : targetsdk; + for (Path jar : sdk.getApiJars()) { + cp.append(mkPath(jar.toString())); + } + for (JCImport i : raw_imports) { + // Support import clauses with only jar or exp values + if (i.jar != null) { + cp.append(mkPath(i.jar)); + } + } + j.execute(); + } + + private void addKitClasses(Java j) { + // classpath to jckit bits + org.apache.tools.ant.types.Path cp = j.createClasspath(); + for (Path jar : jckit.getToolJars()) { + cp.append(mkPath(jar.toString())); + } + j.setClasspath(cp); + } + + private void convert(Path applet_folder, List exps) { + setTaskName("convert"); + // construct java task + Java j = new Java(this); + j.setTaskName("convert"); + j.setFailonerror(true); + j.setFork(true); + + // add classpath for SDK tools + addKitClasses(j); + + // set class depending on SDK + if (jckit.getVersion().isV3()) { + j.setClassname("com.sun.javacard.converter.Main"); + // XXX: See https://community.oracle.com/message/10452555 + Environment.Variable jchome = new Environment.Variable(); + jchome.setKey("jc.home"); + jchome.setValue(jckit.getRoot().toString()); + j.addSysproperty(jchome); + } else { + j.setClassname("com.sun.javacard.converter.Converter"); + } + + // output path + j.createArg().setLine("-d '" + applet_folder + "'"); + + // classes for conversion + j.createArg().setLine("-classdir '" + classes_path + "'"); + + // construct export path + StringJoiner expstringbuilder = new StringJoiner(File.pathSeparator); + + // Add targetSDK export files + if (jckit.getVersion().isOneOf(V310, V320) && targetsdk.getVersion().isOneOf(V304, V305, V310)) { + j.createArg().setLine("-target " + targetsdk.getVersion().toString()); + } else { + expstringbuilder.add(targetsdk.getExportDir().toString()); + } + + // imports + for (Path imp : exps) { + expstringbuilder.add(imp.toString()); + } + j.createArg().setLine("-exportpath '" + expstringbuilder + "'"); + + // always be a little verbose + j.createArg().setLine("-verbose"); + j.createArg().setLine("-nobanner"); + + // simple options + if (debug) { + j.createArg().setLine("-debug"); + } + if (!verify && !jckit.getVersion().isOneOf(V211, V212)) { + j.createArg().setLine("-noverify"); + } + if (jckit.getVersion().isV3()) { + j.createArg().setLine("-useproxyclass"); + } + if (ints) { + j.createArg().setLine("-i"); + } + + // determine output types + String outputs = "CAP"; + if (output_exp != null || (raw_applets.size() > 1 && verify)) { + outputs += " EXP"; + } + if (output_jca != null) { + outputs += " JCA"; + } + j.createArg().setLine("-out " + outputs); + + // define applets + for (JCApplet app : raw_applets) { + j.createArg().setLine("-applet " + Misc.hexAID(app.aid) + " " + app.klass); + } + + // package properties + j.createArg().setLine(package_name + " " + Misc.hexAID(package_aid) + " " + package_version); + + // report the command + log("command: " + j.getCommandLine(), Project.MSG_VERBOSE); + + // execute the converter + j.execute(); + } + + @Override + public void execute() { + Project project = getProject(); + + // perform checks + check(); + + try { + // Compile first if necessary + if (sources_path != null) { + compile(); + } + + // Create temporary folder and add to cleanup + Path applet_folder = makeTemp("applet"); + + // Construct exportpath + ArrayList exps = new ArrayList<>(); + + // add imports + for (JCImport imp : raw_imports) { + // Support import clauses with only jar or exp values + final Path f; + if (imp.exps != null) { + f = Paths.get(imp.exps).toAbsolutePath(); + } else { + try { + // Assume exp files in jar + f = makeTemp("imports"); + OffCardVerifier.extractExps(project.resolveFile(imp.jar).toPath(), f); + } catch (IOException e) { + throw new BuildException("Can not extract EXP files from JAR", e); + } + } + // Avoid duplicates + if (!exps.contains(f)) { + exps.add(f); + } + } + + // perform conversion + convert(applet_folder, exps); + + // Copy results + // Last component of the package + String ln = package_name; + if (ln.lastIndexOf(".") != -1) { + ln = ln.substring(ln.lastIndexOf(".") + 1); + } + // directory of package + String pkgPath = package_name.replace(".", File.separator); + Path pkgDir = applet_folder.resolve(pkgPath); + Path jcsrc = pkgDir.resolve("javacard"); + // Interesting paths inside the JC folder + Path cap = jcsrc.resolve(ln + ".cap"); + Path exp = jcsrc.resolve(ln + ".exp"); + Path jca = jcsrc.resolve(ln + ".jca"); + + // Verify + if (verify) { + setTaskName("verify"); + OffCardVerifier verifier = OffCardVerifier.withSDK(jckit); + // Add current export file + exps.add(exp); + exps.add(targetsdk.getExportDir()); + try { + verifier.verify(cap, exps); + log("Verification passed", Project.MSG_INFO); + } catch (VerifierError | IOException e) { + throw new BuildException("Verification failed: " + e.getMessage()); + } + } + + setTaskName("cap"); + // Copy resources to final destination + try { + // check that a CAP file got created + if (!Files.exists(cap)) { + throw new BuildException("Can not find CAP in " + jcsrc); + } + + // copy CAP file + CAPFile capfile = CAPFile.fromBytes(Files.readAllBytes(cap)); + + // Create output name, if not given. + output_cap = capFileName(capfile, output_cap); + + // resolve output path + Path outCap = project.resolveFile(output_cap).toPath(); + + // strip classes, if asked + if (strip) { + CAPFile.strip(cap); + } + + // perform the copy + Files.copy(cap, outCap, StandardCopyOption.REPLACE_EXISTING); + // report destination + log("CAP saved to " + outCap, Project.MSG_INFO); + + // copy EXP file + if (output_exp != null) { + setTaskName("exp"); + // check that an EXP file got created + if (!Files.exists(exp)) { + throw new BuildException("Can not find EXP in " + jcsrc); + } + // resolve output directory + Path outExp = project.resolveFile(output_exp).toPath(); + // determine package directories + Path outExpPkg = outExp.resolve(pkgPath); + Path outExpPkgJc = outExpPkg.resolve("javacard"); + // create directories + if (!Files.exists(outExpPkgJc)) { + Files.createDirectories(outExpPkgJc); + } + // perform the copy + Path exp_file = outExpPkgJc.resolve(exp.getFileName()); + + Files.copy(exp, exp_file, StandardCopyOption.REPLACE_EXISTING); + // report destination + log("EXP saved to " + exp_file, Project.MSG_INFO); + // add the export directory to the export path for verification + exps.add(outExp); + } + + // copy JCA file + if (output_jca != null) { + setTaskName("jca"); + // check that a JCA file got created + if (!Files.exists(jca)) { + throw new BuildException("Can not find JCA in " + jcsrc); + } + // resolve output path + outCap = project.resolveFile(output_jca).toPath(); + Files.copy(jca, outCap, StandardCopyOption.REPLACE_EXISTING); + log("JCA saved to " + outCap.toAbsolutePath(), Project.MSG_INFO); + } + + // create JAR file + if (output_jar != null) { + setTaskName("jar"); + File outJar = project.resolveFile(output_jar); + // create a new JAR task + Jar jarz = new Jar(); + jarz.setProject(project); + jarz.setTaskName("jar"); + jarz.setDestFile(outJar); + // include class files + FileSet jarcls = new FileSet(); + jarcls.setDir(project.resolveFile(classes_path)); + jarz.add(jarcls); + // include conversion output + FileSet jarout = new FileSet(); + jarout.setDir(applet_folder.toFile()); + jarz.add(jarout); + // create the JAR + jarz.execute(); + log("JAR saved to " + outJar.getAbsolutePath(), Project.MSG_INFO); + } + } catch (IOException e) { + e.printStackTrace(); + throw new BuildException("Can not copy output CAP, EXP or JCA", e); + } + } finally { + JavaCard.cleanTemp(); + } + } + + private Path makeTemp(String sub) { + try { + if (System.getenv("ANT_JAVACARD_TMP") != null) { + Path tmp = Paths.get(System.getenv("ANT_JAVACARD_TMP"), sub); + if (Files.exists(tmp, LinkOption.NOFOLLOW_LINKS)) { + Misc.rmminusrf(tmp); + } + Files.createDirectories(tmp); + return tmp; + } else { + Path p = Files.createTempDirectory("jccpro"); + JavaCard.temporary.add(p); + return p; + } + } catch (IOException e) { + throw new RuntimeException("Can not make temporary folder", e); + } + } + + private static String capFileName(CAPFile cap, String template) { + String name = template; + final String n; + // Fallback if %n is requested with no applets + if (cap.getAppletAIDs().size() == 1 && !cap.getFlags().contains("exports")) { + n = Misc.className(cap.getApplets().entrySet().iterator().next().getValue()); + } else { + n = cap.getPackageName(); + } + + // LFDBH-s + name = name.replace("%H", Misc.encodeHexString(cap.getLoadFileDataHash("SHA-256")).toLowerCase()); + name = name.replace("%h", Misc.encodeHexString(cap.getLoadFileDataHash("SHA-256")).toLowerCase().substring(0, 8)); + name = name.replace("%n", n); // "common name", applet or package + name = name.replace("%p", cap.getPackageName()); // package name + name = name.replace("%a", cap.getPackageAID().toString()); // package AID + name = name.replace("%j", cap.guessJavaCardVersion().orElse("unknown")); // JavaCard version + name = name.replace("%g", cap.guessGlobalPlatformVersion().orElse("unknown")); // GlobalPlatform version + return name; + } +} diff --git a/task/src/main/java/pro/javacard/ant/JCImport.java b/task/src/main/java/pro/javacard/ant/JCImport.java new file mode 100644 index 0000000..1c6a2f2 --- /dev/null +++ b/task/src/main/java/pro/javacard/ant/JCImport.java @@ -0,0 +1,15 @@ +package pro.javacard.ant; + +// Just for Ant +public class JCImport { + String exps = null; + String jar = null; + + public void setExps(String msg) { + exps = msg; + } + + public void setJar(String msg) { + jar = msg; + } +} diff --git a/task/src/main/java/pro/javacard/ant/JavaCard.java b/task/src/main/java/pro/javacard/ant/JavaCard.java index e4349c7..6b3c093 100644 --- a/task/src/main/java/pro/javacard/ant/JavaCard.java +++ b/task/src/main/java/pro/javacard/ant/JavaCard.java @@ -21,121 +21,26 @@ */ package pro.javacard.ant; -import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.Task; -import org.apache.tools.ant.taskdefs.Jar; -import org.apache.tools.ant.taskdefs.Java; -import org.apache.tools.ant.taskdefs.Javac; -import org.apache.tools.ant.types.Environment.Variable; -import org.apache.tools.ant.types.FileSet; -import pro.javacard.capfile.CAPFile; -import pro.javacard.sdk.JavaCardSDK; -import pro.javacard.sdk.OffCardVerifier; -import pro.javacard.sdk.SDKVersion; -import pro.javacard.sdk.VerifierError; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; import java.nio.file.*; -import java.nio.file.attribute.BasicFileAttributes; import java.util.*; -import java.util.regex.Pattern; - -import static pro.javacard.sdk.SDKVersion.*; +// +// This is a wrapper task that can contain one or more subtasks. public final class JavaCard extends Task { - private List temporary = new ArrayList<>(); - // This code has been taken from Apache commons-codec 1.7 (License: Apache - // 2.0) - private static final char[] LOWER_HEX = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + static List temporary = new ArrayList<>(); + private String master_jckit_path = null; private Vector packages = new Vector<>(); - private static String hexAID(byte[] aid) { - StringBuffer hexaid = new StringBuffer(); - for (byte b : aid) { - hexaid.append(String.format("0x%02X", b)); - hexaid.append(":"); - } - String hex = hexaid.toString(); - // Cut off the final colon - return hex.substring(0, hex.length() - 1); - } - - // For cleaning up temporary files - private static void rmminusrf(Path path) { - try { - Files.walkFileTree(path, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) - throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException e) - throws IOException { - if (e == null) { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } else { - // directory iteration failed - throw e; - } - } - }); - } catch (FileNotFoundException | NoSuchFileException e) { - // Already gone - do nothing. - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public static String encodeHexString(final byte[] data) { - final int l = data.length; - final char[] out = new char[l << 1]; - // two characters form the hex value. - for (int i = 0, j = 0; i < l; i++) { - out[j++] = LOWER_HEX[(0xF0 & data[i]) >>> 4]; - out[j++] = LOWER_HEX[0x0F & data[i]]; - } - return new String(out); - } - - public static byte[] decodeHexString(String str) { - char data[] = str.toCharArray(); - final int len = data.length; - if ((len & 0x01) != 0) { - throw new IllegalArgumentException("Odd number of characters: " + str); - } - final byte[] out = new byte[len >> 1]; - // two characters form the hex value. - for (int i = 0, j = 0; j < len; i++) { - int f = Character.digit(data[j], 16) << 4; - j++; - f = f | Character.digit(data[j], 16); - j++; - out[i] = (byte) (f & 0xFF); - } - return out; - } - - public static byte[] stringToBin(String s) { - s = s.toLowerCase().replaceAll(" ", "").replaceAll(":", ""); - s = s.replaceAll("0x", "").replaceAll("\n", "").replaceAll("\t", ""); - s = s.replaceAll(";", ""); - return decodeHexString(s); - } - public void setJCKit(String msg) { master_jckit_path = msg; } public JCCap createCap() { - JCCap pkg = new JCCap(); + JCCap pkg = new JCCap(master_jckit_path); packages.add(pkg); return pkg; } @@ -156,797 +61,16 @@ public void execute() { } } - private void cleanTemp() { + static void cleanTemp() { + // Do not clean temporary files if manually set temporary path is set. This is useful for debugging. if (System.getenv("ANT_JAVACARD_TMP") != null) return; + // Clean temporary files. for (Path f : temporary) { if (Files.exists(f)) { - rmminusrf(f); + Misc.rmminusrf(f); } } } - - public static class JCApplet { - private String klass = null; - private byte[] aid = null; - - public JCApplet() { - } - - public void setClass(String msg) { - klass = msg; - } - - public void setAID(String msg) { - try { - aid = stringToBin(msg); - if (aid.length < 5 || aid.length > 16) { - throw new BuildException("Applet AID must be between 5 and 16 bytes: " + aid.length); - } - } catch (IllegalArgumentException e) { - throw new BuildException("Not a valid applet AID: " + e.getMessage()); - } - } - } - - public static class HelpingBuildException extends BuildException { - private final static long serialVersionUID = -2365126253968479314L; - public HelpingBuildException(String msg) { - super(msg + "\n\nPLEASE READ https://github.com/martinpaljak/ant-javacard#readme"); - } - } - - public class JCCap extends Task { - private JavaCardSDK jckit = null; - private String classes_path = null; - private String sources_path = null; - private String sources2_path = null; - private String includes = null; - private String excludes = null; - private String package_name = null; - private byte[] package_aid = null; - private String package_version = null; - private Vector raw_applets = new Vector<>(); - private Vector raw_imports = new Vector<>(); - private String output_cap = null; - private String output_exp = null; - private String output_jar = null; - private String output_jca = null; - private String jckit_path = null; - private JavaCardSDK targetsdk = null; - private String raw_targetsdk = null; - - private boolean verify = true; - private boolean debug = false; - private boolean strip = false; - private boolean ints = false; - - - public JCCap() { - } - - public void setJCKit(String msg) { - jckit_path = msg; - } - - public void setOutput(String msg) { - output_cap = msg; - } - - public void setExport(String msg) { - output_exp = msg; - } - - public void setJar(String msg) { - output_jar = msg; - } - - public void setJca(String msg) { - output_jca = msg; - } - - public void setPackage(String msg) { - package_name = msg; - } - - public void setClasses(String msg) { - classes_path = msg; - } - - public void setVersion(String msg) { - package_version = msg; - } - - public void setSources(String arg) { - sources_path = arg; - } - - public void setSources2(String arg) { - sources2_path = arg; - } - - public void setIncludes(String arg) { - includes = arg; - } - - public void setExcludes(String arg) { - excludes = arg; - } - - public void setVerify(boolean arg) { - verify = arg; - } - - public void setDebug(boolean arg) { - debug = arg; - } - - public void setStrip(boolean arg) { - strip = arg; - } - - public void setInts(boolean arg) { - ints = arg; - } - - public void setTargetsdk(String arg) { - raw_targetsdk = arg; - } - - public void setAID(String msg) { - try { - package_aid = stringToBin(msg); - if (package_aid.length < 5 || package_aid.length > 16) - throw new BuildException("Package AID must be between 5 and 16 bytes: " + encodeHexString(package_aid) + " (" + package_aid.length + ")"); - - } catch (IllegalArgumentException e) { - throw new BuildException("Not a correct package AID: " + e.getMessage()); - } - } - - // Many applets inside one package - public JCApplet createApplet() { - JCApplet applet = new JCApplet(); - raw_applets.add(applet); - return applet; - } - - // Many imports inside one package - public JCImport createImport() { - JCImport imp = new JCImport(); - raw_imports.add(imp); - return imp; - } - - // To support usage from Gradle, where import is a reserved name - public JCImport createJimport() { - return this.createImport(); - } - - private Optional findSDK() { - // try configuration first - if (jckit_path != null) { - return JavaCardSDK.detectSDK(getProject().resolveFile(jckit_path).toPath()); - } - if (master_jckit_path != null) { - return JavaCardSDK.detectSDK(getProject().resolveFile(master_jckit_path).toPath()); - } - // now check via ant property - String propPath = getProject().getProperty("jc.home"); - if (propPath != null) { - return JavaCardSDK.detectSDK(getProject().resolveFile(propPath).toPath()); - } - // finally via the environment - String envPath = System.getenv("JC_HOME"); - if (envPath != null) { - return JavaCardSDK.detectSDK(getProject().resolveFile(envPath).toPath()); - } - // return null if no options - return Optional.empty(); - } - - // Check that arguments are sufficient and do some DWIM - private void check() { - jckit = findSDK().orElseThrow(() -> new HelpingBuildException("No usable JavaCard SDK referenced")); - - log("INFO: using JavaCard " + jckit.getVersion() + " SDK in " + jckit.getRoot() + " with JDK " + getCurrentJDKVersion(), Project.MSG_INFO); - - if (raw_targetsdk != null) { - Optional targetVersion = SDKVersion.fromVersion(raw_targetsdk); - if (jckit != null && jckit.getVersion().isOneOf(V310, V320) && targetVersion.isPresent()) { - SDKVersion target = targetVersion.get(); - // FIXME: can't target 3.2.0 with 3.1.0 - if (target.isOneOf(V304, V305, V310, V320)) { - targetsdk = jckit.target(target); - } else { - throw new HelpingBuildException("Can not target JavaCard " + target + " with JavaCard kit " + jckit.getVersion()); - } - } else { - targetsdk = JavaCardSDK.detectSDK(getProject().resolveFile(raw_targetsdk).toPath()).orElseThrow(() -> new HelpingBuildException("Invalid targetsdk: " + raw_targetsdk)); - if (jckit.getVersion() == V310 && !targetsdk.getVersion().isOneOf(V304, V305, V310)) { - throw new HelpingBuildException("targetsdk " + targetsdk.getVersion() + " is not compatible with jckit " + jckit.getVersion()); - } - } - } - - if (targetsdk == null) { - targetsdk = jckit; - } else { - log("INFO: targeting JavaCard " + targetsdk.getVersion() + " SDK in " + targetsdk.getRoot(), Project.MSG_INFO); - } - - // Warn about deprecation in future - if (sources_path != null && sources2_path != null) { - log("WARN: sources2 is deprecated in favor of multiple paths in sources", Project.MSG_WARN); - } - - // Shorthand for simple small projects - use Maven conventions - if (sources_path == null && classes_path == null) { - if (getProject().resolveFile("src/main/javacard").isDirectory()) - sources_path = "src/main/javacard"; - else if (getProject().resolveFile("src/main/java").isDirectory()) - sources_path = "src/main/java"; - } - - // sources or classes must be set - if (sources_path == null && classes_path == null) { - throw new HelpingBuildException("Must specify sources or classes"); - } - - // Check package version - if (package_version == null) { - package_version = "0.0"; - } else { - // Allowed values are 0..127 - if (!package_version.matches("^[0-9]{1,3}\\.[0-9]{1,3}$")) { - throw new HelpingBuildException("Invalid package version: " + package_version); - } - Arrays.asList(package_version.split("\\.")).stream().map(e -> Integer.parseInt(e, 10)).forEach(e -> { - if (e < 0 || e > 127) - throw new HelpingBuildException("Illegal package version value: " + package_version); - }); - } - - // Check imports - for (JCImport a : raw_imports) { - if (a.jar != null && !getProject().resolveFile(a.jar).isFile()) - throw new BuildException("Import JAR does not exist: " + a.jar); - if (a.exps != null && !getProject().resolveFile(a.exps).isDirectory()) - throw new BuildException("Import EXP files folder does not exist: " + a.exps); - } - // Construct applets and fill in missing bits from package info, if necessary - int applet_counter = 0; - for (JCApplet a : raw_applets) { - // Keep count for automagic numbering - applet_counter = applet_counter + 1; - - if (a.klass == null) { - throw new HelpingBuildException("Applet class is missing"); - } - // If package name is present, must match the applet - if (package_name != null) { - if (!a.klass.contains(".")) { - a.klass = package_name + "." + a.klass; - } else if (!a.klass.startsWith(package_name)) { - throw new HelpingBuildException("Applet class " + a.klass + " is not in package " + package_name); - } - } else { - if (a.klass.contains(".")) { - String pkgname = a.klass.substring(0, a.klass.lastIndexOf(".")); - log("INFO: Setting package name to " + pkgname, Project.MSG_INFO); - package_name = pkgname; - } else { - throw new HelpingBuildException("Applet must be in a package!"); - } - } - - // If applet AID is present, must match the package AID - if (package_aid != null) { - if (a.aid != null) { - // RID-s must match - if (!Arrays.equals(Arrays.copyOf(package_aid, 5), Arrays.copyOf(a.aid, 5))) { - throw new HelpingBuildException("Package RID does not match Applet RID"); - } - } else { - // make "magic" applet AID from package_aid + counter - a.aid = Arrays.copyOf(package_aid, package_aid.length + 1); - a.aid[package_aid.length] = (byte) applet_counter; - log("INFO: generated applet AID: " + encodeHexString(a.aid) + " for " + a.klass, Project.MSG_INFO); - } - } else { - // if package AID is empty, just set it to the minimal from - // applet - if (a.aid != null) { - package_aid = Arrays.copyOf(a.aid, 5); - } else { - throw new HelpingBuildException("Both package AID and applet AID are missing!"); - } - } - } - - // Check package AID - if (package_aid == null) { - throw new HelpingBuildException("Must specify package AID"); - } - - // Package name must be present if no applets - if (raw_applets.size() == 0) { - if (package_name == null) - throw new HelpingBuildException("Must specify package name if no applets"); - log("Building library from package " + package_name + " (AID: " + encodeHexString(package_aid) + ")", Project.MSG_INFO); - } else { - log("Building CAP with " + applet_counter + " applet" + (applet_counter > 1 ? "s" : "") + " from package " + package_name + " (AID: " + encodeHexString(package_aid) + ")", Project.MSG_INFO); - for (JCApplet app : raw_applets) { - log(app.klass + " " + encodeHexString(app.aid), Project.MSG_INFO); - } - } - if (output_exp != null) { - // Last component of the package - String ln = package_name; - if (ln.lastIndexOf(".") != -1) { - ln = ln.substring(ln.lastIndexOf(".") + 1); - } - output_jar = new File(output_exp, ln + ".jar").toString(); - } - // Default output name - if (output_cap == null) { - output_cap = "%n_%a_%h_%j.cap"; // SomeApplet_010203040506_9a037e30_2.2.2.cap - } - } - - // To lessen the java.nio and apache.ant namespace clash... - private org.apache.tools.ant.types.Path mkPath(String name) { - if (name == null) - return new org.apache.tools.ant.types.Path(getProject()); - return new org.apache.tools.ant.types.Path(getProject(), name); - } - - private void compile() { - Project project = getProject(); - setTaskName("compile"); - - // construct javac task - Javac j = new Javac(); - j.setProject(project); - j.setTaskName("compile"); - - org.apache.tools.ant.types.Path sources = mkPath(null); - - // New style - multiple folders - String pattern = Pattern.quote(File.pathSeparator); - String[] sources_paths = sources_path.split(pattern); - for (String path : sources_paths) - sources.append(mkPath(path)); - - // Old style - second folder - if (sources2_path != null) - sources.append(mkPath(sources2_path)); - j.setSrcdir(sources); - - if (includes != null) { - j.setIncludes(includes); - } - - if (excludes != null) { - j.setExcludes(excludes); - } - - // We resolve files to compile based on the sources/includes/excludes parameters, so don't set sourcepath - j.setSourcepath(new org.apache.tools.ant.types.Path(project, null)); - - log("Compiling files from " + sources, Project.MSG_INFO); - - // determine output directory - Path tmp; - if (classes_path != null) { - // if specified use that - tmp = project.resolveFile(classes_path).toPath(); - if (!Files.exists(tmp)) { - try { - Files.createDirectories(tmp); - } catch (IOException e) { - throw new BuildException("Could not create classes folder " + tmp.toAbsolutePath()); - } - } - } else { - // else generate temporary folder - tmp = makeTemp("classes"); - classes_path = tmp.toAbsolutePath().toString(); - } - - j.setDestdir(tmp.toFile()); - // See "Setting Java Compiler Options" in User Guide - j.setDebug(true); - j.setDebugLevel("lines,vars,source"); - - // set the best option supported by jckit - String javaVersion = JavaCardSDK.getJavaVersion(jckit.getVersion()); - // Warn in human readable way if Java not compatible with JC Kit - // See https://github.com/martinpaljak/ant-javacard/issues/79 - int jdkver = getCurrentJDKVersion(); - if (jdkver > 17) { - throw new HelpingBuildException("JDK 17 LTS is the latest supported JDK."); - } else if (jckit.getVersion().isOneOf(V211, V212, V221, V222) && jdkver > 8) { - throw new HelpingBuildException("Use JDK 8 with JavaCard kit v2.x"); - } else if (jdkver > 11 && !jckit.getVersion().isOneOf(V310, V320)) { - throw new HelpingBuildException("Use JDK 11 with JavaCard kit " + jckit.getVersion()); - } - - j.setTarget(javaVersion); - j.setSource(javaVersion); - - j.setIncludeantruntime(false); - j.createCompilerArg().setValue("-Xlint"); - j.createCompilerArg().setValue("-Xlint:-options"); - j.createCompilerArg().setValue("-Xlint:-serial"); - if (jckit.getVersion().isOneOf(V304, V305, V310)) { - //-processor com.oracle.javacard.stringproc.StringConstantsProcessor \ - // -processorpath "JCDK_HOME/lib/tools.jar;JCDK_HOME/lib/api_classic_annotations.jar" \ - j.createCompilerArg().setLine("-processor com.oracle.javacard.stringproc.StringConstantsProcessor"); - org.apache.tools.ant.types.Path pcp = new Javac().createClasspath(); - for (Path jar : jckit.getCompilerJars()) { - pcp.append(mkPath(jar.toString())); - } - j.createCompilerArg().setLine("-processorpath \"" + pcp.toString() + "\""); - j.createCompilerArg().setValue("-Xlint:all,-processing"); - } - - j.setFailonerror(true); - j.setFork(true); - j.setListfiles(true); - - // set classpath - org.apache.tools.ant.types.Path cp = j.createClasspath(); - JavaCardSDK sdk = targetsdk == null ? jckit : targetsdk; - for (Path jar : sdk.getApiJars()) { - cp.append(mkPath(jar.toString())); - } - for (JCImport i : raw_imports) { - // Support import clauses with only jar or exp values - if (i.jar != null) { - cp.append(mkPath(i.jar)); - } - } - j.execute(); - } - - private void addKitClasses(Java j) { - // classpath to jckit bits - org.apache.tools.ant.types.Path cp = j.createClasspath(); - for (Path jar : jckit.getToolJars()) { - cp.append(mkPath(jar.toString())); - } - j.setClasspath(cp); - } - - private void convert(Path applet_folder, List exps) { - setTaskName("convert"); - // construct java task - Java j = new Java(this); - j.setTaskName("convert"); - j.setFailonerror(true); - j.setFork(true); - - // add classpath for SDK tools - addKitClasses(j); - - // set class depending on SDK - if (jckit.getVersion().isV3()) { - j.setClassname("com.sun.javacard.converter.Main"); - // XXX: See https://community.oracle.com/message/10452555 - Variable jchome = new Variable(); - jchome.setKey("jc.home"); - jchome.setValue(jckit.getRoot().toString()); - j.addSysproperty(jchome); - } else { - j.setClassname("com.sun.javacard.converter.Converter"); - } - - // output path - j.createArg().setLine("-d '" + applet_folder + "'"); - - // classes for conversion - j.createArg().setLine("-classdir '" + classes_path + "'"); - - // construct export path - StringJoiner expstringbuilder = new StringJoiner(File.pathSeparator); - - // Add targetSDK export files - if (jckit.getVersion().isOneOf(V310, V320) && targetsdk.getVersion().isOneOf(V304, V305, V310)) { - j.createArg().setLine("-target " + targetsdk.getVersion().toString()); - } else { - expstringbuilder.add(targetsdk.getExportDir().toString()); - } - - // imports - for (Path imp : exps) { - expstringbuilder.add(imp.toString()); - } - j.createArg().setLine("-exportpath '" + expstringbuilder + "'"); - - // always be a little verbose - j.createArg().setLine("-verbose"); - j.createArg().setLine("-nobanner"); - - // simple options - if (debug) { - j.createArg().setLine("-debug"); - } - if (!verify && !jckit.getVersion().isOneOf(V211, V212)) { - j.createArg().setLine("-noverify"); - } - if (jckit.getVersion().isV3()) { - j.createArg().setLine("-useproxyclass"); - } - if (ints) { - j.createArg().setLine("-i"); - } - - // determine output types - String outputs = "CAP"; - if (output_exp != null || (raw_applets.size() > 1 && verify)) { - outputs += " EXP"; - } - if (output_jca != null) { - outputs += " JCA"; - } - j.createArg().setLine("-out " + outputs); - - // define applets - for (JCApplet app : raw_applets) { - j.createArg().setLine("-applet " + hexAID(app.aid) + " " + app.klass); - } - - // package properties - j.createArg().setLine(package_name + " " + hexAID(package_aid) + " " + package_version); - - // report the command - log("command: " + j.getCommandLine(), Project.MSG_VERBOSE); - - // execute the converter - j.execute(); - } - - @Override - public void execute() { - Project project = getProject(); - - // perform checks - check(); - - try { - // Compile first if necessary - if (sources_path != null) { - compile(); - } - - // Create temporary folder and add to cleanup - Path applet_folder = makeTemp("applet"); - - // Construct exportpath - ArrayList exps = new ArrayList<>(); - - // add imports - for (JCImport imp : raw_imports) { - // Support import clauses with only jar or exp values - final Path f; - if (imp.exps != null) { - f = Paths.get(imp.exps).toAbsolutePath(); - } else { - try { - // Assume exp files in jar - f = makeTemp("imports"); - OffCardVerifier.extractExps(project.resolveFile(imp.jar).toPath(), f); - } catch (IOException e) { - throw new BuildException("Can not extract EXP files from JAR", e); - } - } - // Avoid duplicates - if (!exps.contains(f)) { - exps.add(f); - } - } - - // perform conversion - convert(applet_folder, exps); - - // Copy results - // Last component of the package - String ln = package_name; - if (ln.lastIndexOf(".") != -1) { - ln = ln.substring(ln.lastIndexOf(".") + 1); - } - // directory of package - String pkgPath = package_name.replace(".", File.separator); - Path pkgDir = applet_folder.resolve(pkgPath); - Path jcsrc = pkgDir.resolve("javacard"); - // Interesting paths inside the JC folder - Path cap = jcsrc.resolve(ln + ".cap"); - Path exp = jcsrc.resolve(ln + ".exp"); - Path jca = jcsrc.resolve(ln + ".jca"); - - // Verify - if (verify) { - setTaskName("verify"); - OffCardVerifier verifier = OffCardVerifier.withSDK(jckit); - // Add current export file - exps.add(exp); - exps.add(targetsdk.getExportDir()); - try { - verifier.verify(cap, exps); - log("Verification passed", Project.MSG_INFO); - } catch (VerifierError | IOException e) { - throw new BuildException("Verification failed: " + e.getMessage()); - } - } - - setTaskName("cap"); - // Copy resources to final destination - try { - // check that a CAP file got created - if (!Files.exists(cap)) { - throw new BuildException("Can not find CAP in " + jcsrc); - } - - // copy CAP file - CAPFile capfile = CAPFile.fromBytes(Files.readAllBytes(cap)); - - // Create output name, if not given. - output_cap = capFileName(capfile, output_cap); - - // resolve output path - Path outCap = project.resolveFile(output_cap).toPath(); - - // strip classes, if asked - if (strip) { - CAPFile.strip(cap); - } - - // perform the copy - Files.copy(cap, outCap, StandardCopyOption.REPLACE_EXISTING); - // report destination - log("CAP saved to " + outCap, Project.MSG_INFO); - - // copy EXP file - if (output_exp != null) { - setTaskName("exp"); - // check that an EXP file got created - if (!Files.exists(exp)) { - throw new BuildException("Can not find EXP in " + jcsrc); - } - // resolve output directory - Path outExp = project.resolveFile(output_exp).toPath(); - // determine package directories - Path outExpPkg = outExp.resolve(pkgPath); - Path outExpPkgJc = outExpPkg.resolve("javacard"); - // create directories - if (!Files.exists(outExpPkgJc)) { - Files.createDirectories(outExpPkgJc); - } - // perform the copy - Path exp_file = outExpPkgJc.resolve(exp.getFileName()); - - Files.copy(exp, exp_file, StandardCopyOption.REPLACE_EXISTING); - // report destination - log("EXP saved to " + exp_file, Project.MSG_INFO); - // add the export directory to the export path for verification - exps.add(outExp); - } - - // copy JCA file - if (output_jca != null) { - setTaskName("jca"); - // check that a JCA file got created - if (!Files.exists(jca)) { - throw new BuildException("Can not find JCA in " + jcsrc); - } - // resolve output path - outCap = project.resolveFile(output_jca).toPath(); - Files.copy(jca, outCap, StandardCopyOption.REPLACE_EXISTING); - log("JCA saved to " + outCap.toAbsolutePath(), Project.MSG_INFO); - } - - // create JAR file - if (output_jar != null) { - setTaskName("jar"); - File outJar = project.resolveFile(output_jar); - // create a new JAR task - Jar jarz = new Jar(); - jarz.setProject(project); - jarz.setTaskName("jar"); - jarz.setDestFile(outJar); - // include class files - FileSet jarcls = new FileSet(); - jarcls.setDir(project.resolveFile(classes_path)); - jarz.add(jarcls); - // include conversion output - FileSet jarout = new FileSet(); - jarout.setDir(applet_folder.toFile()); - jarz.add(jarout); - // create the JAR - jarz.execute(); - log("JAR saved to " + outJar.getAbsolutePath(), Project.MSG_INFO); - } - } catch (IOException e) { - e.printStackTrace(); - throw new BuildException("Can not copy output CAP, EXP or JCA", e); - } - } finally { - cleanTemp(); - } - } - - private Path makeTemp(String sub) { - try { - if (System.getenv("ANT_JAVACARD_TMP") != null) { - Path tmp = Paths.get(System.getenv("ANT_JAVACARD_TMP"), sub); - if (Files.exists(tmp, LinkOption.NOFOLLOW_LINKS)) { - rmminusrf(tmp); - } - Files.createDirectories(tmp); - return tmp; - } else { - Path p = Files.createTempDirectory("jccpro"); - temporary.add(p); - return p; - } - } catch (IOException e) { - throw new RuntimeException("Can not make temporary folder", e); - } - } - - private String capFileName(CAPFile cap, String template) { - String name = template; - final String n; - // Fallback if %n is requested with no applets - if (cap.getAppletAIDs().size() == 1 && !cap.getFlags().contains("exports")) { - n = className(raw_applets.get(0).klass); - } else { - n = cap.getPackageName(); - } - - // LFDBH-s - name = name.replace("%H", encodeHexString(cap.getLoadFileDataHash("SHA-256")).toLowerCase()); - name = name.replace("%h", encodeHexString(cap.getLoadFileDataHash("SHA-256")).toLowerCase().substring(0, 8)); - name = name.replace("%n", n); // "common name", applet or package - name = name.replace("%p", cap.getPackageName()); // package name - name = name.replace("%a", cap.getPackageAID().toString()); // package AID - name = name.replace("%j", cap.guessJavaCardVersion().orElse("unknown")); // JavaCard version - name = name.replace("%g", cap.guessGlobalPlatformVersion().orElse("unknown")); // GlobalPlatform version - return name; - } - } - - - public static class JCImport { - String exps = null; - String jar = null; - - public void setExps(String msg) { - exps = msg; - } - - public void setJar(String msg) { - jar = msg; - } - } - - private static String className(String fqdn) { - String ln = fqdn; - if (ln.lastIndexOf(".") != -1) { - ln = ln.substring(ln.lastIndexOf(".") + 1); - } - return ln; - } - - // Dirty way to get major version of JDK: 8, 11, 17 - private static int getCurrentJDKVersion() { - String v = System.getProperty("java.version", "0.0.0"); - if (v.startsWith("1.8.")) - v = "8." + v.substring(4); - int dot = v.indexOf("."); - int m = Integer.parseInt(v.substring(0, dot == -1 ? v.length() : dot)); - return m; - } } diff --git a/task/src/main/java/pro/javacard/ant/Misc.java b/task/src/main/java/pro/javacard/ant/Misc.java new file mode 100644 index 0000000..9ffb7a0 --- /dev/null +++ b/task/src/main/java/pro/javacard/ant/Misc.java @@ -0,0 +1,109 @@ +package pro.javacard.ant; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; + +final class Misc { + + // This code has been taken from Apache commons-codec 1.7 (License: Apache 2.0) + private static final char[] LOWER_HEX = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + static String encodeHexString(final byte[] data) { + final int l = data.length; + final char[] out = new char[l << 1]; + // two characters form the hex value. + for (int i = 0, j = 0; i < l; i++) { + out[j++] = LOWER_HEX[(0xF0 & data[i]) >>> 4]; + out[j++] = LOWER_HEX[0x0F & data[i]]; + } + return new String(out); + } + + static byte[] decodeHexString(String str) { + char data[] = str.toCharArray(); + final int len = data.length; + if ((len & 0x01) != 0) { + throw new IllegalArgumentException("Odd number of characters: " + str); + } + final byte[] out = new byte[len >> 1]; + // two characters form the hex value. + for (int i = 0, j = 0; j < len; i++) { + int f = Character.digit(data[j], 16) << 4; + j++; + f = f | Character.digit(data[j], 16); + j++; + out[i] = (byte) (f & 0xFF); + } + return out; + } + + + // Dirty way to get major version of JDK: 8, 11, 17 + static int getCurrentJDKVersion() { + String v = System.getProperty("java.version", "0.0.0"); + if (v.startsWith("1.8.")) + v = "8." + v.substring(4); + int dot = v.indexOf("."); + int m = Integer.parseInt(v.substring(0, dot == -1 ? v.length() : dot)); + return m; + } + + static String hexAID(byte[] aid) { + StringBuffer hexaid = new StringBuffer(); + for (byte b : aid) { + hexaid.append(String.format("0x%02X", b)); + hexaid.append(":"); + } + String hex = hexaid.toString(); + // Cut off the final colon + return hex.substring(0, hex.length() - 1); + } + + // For cleaning up temporary files + static void rmminusrf(Path path) { + try { + Files.walkFileTree(path, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException e) + throws IOException { + if (e == null) { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } else { + // directory iteration failed + throw e; + } + } + }); + } catch (FileNotFoundException | NoSuchFileException e) { + // Already gone - do nothing. + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static byte[] stringToBin(String s) { + s = s.toLowerCase().replaceAll(" ", "").replaceAll(":", ""); + s = s.replaceAll("0x", "").replaceAll("\n", "").replaceAll("\t", ""); + s = s.replaceAll(";", ""); + return decodeHexString(s); + } + + // foo.bar.Baz -> Baz; Foo -> Foo + static String className(String fqdn) { + String ln = fqdn; + if (ln.lastIndexOf(".") != -1) { + ln = ln.substring(ln.lastIndexOf(".") + 1); + } + return ln; + } +}