![JAR search and dependency download from the Maven repository](/logo.png)
pro.javacard.ant.JavaCard Maven / Gradle / Ivy
Show all versions of ant-javacard Show documentation
/**
* Copyright (c) 2015-2018 Martin Paljak
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
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 org.apache.tools.ant.types.Path;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
public final class JavaCard extends Task {
// 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'};
private String master_jckit_path = null;
private String master_java_version = 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);
}
private static void rmminusrf(java.nio.file.Path path) {
try {
Files.walkFileTree(path, new SimpleFileVisitor() {
@Override
public FileVisitResult visitFile(java.nio.file.Path file, BasicFileAttributes attrs)
throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(java.nio.file.Path dir, IOException e)
throws IOException {
if (e == null) {
Files.delete(dir);
return FileVisitResult.CONTINUE;
} else {
// directory iteration failed
throw e;
}
}
});
} 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 void setJavaVersion(String msg) {
master_java_version = msg;
}
public JCCap createCap() {
JCCap pkg = new JCCap();
packages.add(pkg);
return pkg;
}
@Override
public void execute() {
for (JCCap p : packages) {
p.execute();
}
}
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 correct applet AID: " + e.getMessage());
}
}
}
public static String join(Collection col, String glue){
return join(col, glue, null, null);
}
public static String join(Collection col, String glue, String prefix, String suffix){
final int size = col.size();
final StringBuilder bld = new StringBuilder();
if (prefix != null){
bld.append(prefix);
}
int ctr = -1;
for(Object obj : col){
ctr += 1;
bld.append(obj.toString());
if (ctr - 1 < size){
bld.append(glue);
}
}
if (suffix != null){
bld.append(suffix);
}
return bld.toString();
}
@SuppressWarnings("serial")
public static class HelpingBuildException extends BuildException {
public HelpingBuildException(String msg) {
super(msg + "\n\nPLEASE READ https://github.com/martinpaljak/ant-javacard#syntax");
}
}
public class JCCap extends Task {
private JCKit jckit = null;
private String classes_path = null;
private String sources_path = 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 String java_version = null;
private boolean verify = true;
private boolean debug = false;
private boolean ints = false;
private List temporary = new ArrayList<>();
public JCCap() {
}
public void setJCKit(String msg) {
jckit_path = msg;
}
public void setJavaVersion(String msg) {
java_version = 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 setVerify(boolean arg) {
verify = arg;
}
public void setDebug(boolean arg) {
debug = arg;
}
public void setInts(boolean arg) {
ints = 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: " + 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 JCKit findSDK() {
// try configuration first
if(jckit_path != null) {
return JCKit.detectSDK(jckit_path);
}
if(master_jckit_path != null) {
return JCKit.detectSDK(master_jckit_path);
}
// now check via ant property
String propPath = getProject().getProperty("jc.home");
if(propPath != null) {
return JCKit.detectSDK(propPath);
}
// finally via the environment
String envPath = System.getenv("JC_HOME");
if(envPath != null) {
return JCKit.detectSDK(envPath);
}
// return null if no options
return null;
}
// Check that arguments are sufficient and do some DWIM
private void check() {
jckit = findSDK();
// Sanity check
if (jckit == null) {
throw new HelpingBuildException("No usable JavaCard SDK referenced");
} else {
log("INFO: using JavaCard " + jckit.getVersion() + " SDK in " + jckit.getRoot(), Project.MSG_INFO);
}
// 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 {
if (!package_version.matches("^[0-9].[0-9]$")) {
throw new HelpingBuildException("Incorrect package version: " + package_version);
}
}
// Construct applets and fill in missing bits from package info, if
int applet_counter = 0;
// necessary
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 {
String pkgname = a.klass.substring(0, a.klass.lastIndexOf("."));
log("Setting package name to " + pkgname, Project.MSG_INFO);
package_name = pkgname;
}
// 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: " + hexAID(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");
}
// Check output file
if (output_cap == null) {
throw new HelpingBuildException("Must specify output file");
}
// Nice info
log("Building CAP with " + applet_counter + " applet" + (applet_counter > 1 ? "s" : "") + " from package " + package_name, Project.MSG_INFO);
for (JCApplet app : raw_applets) {
log(app.klass + " " + encodeHexString(app.aid), Project.MSG_INFO);
}
}
private void compile() {
Project project = getProject();
setTaskName("compile");
// construct javac task
Javac j = new Javac();
j.setProject(project);
j.setTaskName("compile");
j.setSrcdir(new Path(project, sources_path));
// determine output directory
File tmp;
if (classes_path != null) {
// if specified use that
tmp = project.resolveFile(classes_path);
if (!tmp.exists()) {
if (!tmp.mkdir())
throw new BuildException("Could not create temporary folder " + tmp.getAbsolutePath());
}
} else {
// else generate temporary folder
tmp = makeTemp();
classes_path = tmp.getAbsolutePath();
}
j.setDestdir(tmp);
// See "Setting Java Compiler Options" in User Guide
j.setDebug(true);
String javaVersion = jckit.getJavaVersion();
if(java_version != null) {
javaVersion = java_version;
} else {
if(master_java_version != null) {
javaVersion = master_java_version;
}
}
j.setTarget(javaVersion);
j.setSource(javaVersion);
if (jckit.isVersion(JCKit.Version.V21)) {
// Always set debug to disable "contains local variables,
// but not local variable table." messages
j.setDebug(true);
}
j.setIncludeantruntime(false);
j.createCompilerArg().setValue("-Xlint");
j.createCompilerArg().setValue("-Xlint:-options");
j.createCompilerArg().setValue("-Xlint:-serial");
j.setFailonerror(true);
j.setFork(true);
// set classpath
Path cp = j.createClasspath();
String api = jckit.getApiJar().toString();
cp.append(new Path(project, api));
for (JCImport i : raw_imports) {
// Support import clauses with only jar or exp values
if (i.jar != null) {
cp.append(new Path(project, i.jar));
}
}
j.execute();
}
private void addKitClasses(Java j) {
Project project = getProject();
// classpath to jckit bits
Path cp = j.createClasspath();
for(File jar: jckit.getToolJars()) {
cp.append(new Path(project, jar.getPath()));
}
j.setClasspath(cp);
}
private void convert(File 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.isVersion(JCKit.Version.V3)) {
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.getAbsolutePath() + "'");
// classes for conversion
j.createArg().setLine("-classdir '" + classes_path + "'");
// construct export path
final String paths = join(exps, File.pathSeparator);
j.createArg().setLine("-exportpath '" + paths + "'");
// always be a little verbose
j.createArg().setLine("-verbose");
j.createArg().setLine("-nobanner");
// simple options
if (debug) {
j.createArg().setLine("-debug");
}
if (!verify) {
j.createArg().setLine("-noverify");
}
if (jckit.isVersion(JCKit.Version.V3)) {
j.createArg().setLine("-useproxyclass");
}
if (ints) {
j.createArg().setLine("-i");
}
// determine output types
String outputs = "CAP";
if (output_exp != null) {
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();
}
private void verify(List exps) {
Project project = getProject();
setTaskName("verify");
// collect all export files
final ArrayList expfiles = new ArrayList<>();
try {
for (File e : exps) {
Files.walkFileTree(e.toPath(), new SimpleFileVisitor() {
@Override
public FileVisitResult visitFile(java.nio.file.Path file, BasicFileAttributes attrs)
throws IOException {
if (file.toString().endsWith(".exp")) {
expfiles.add(file.toFile());
}
return FileVisitResult.CONTINUE;
}
});
}
} catch (IOException e) {
log("Could not find .exp files: " + e.getMessage(), Project.MSG_ERR);
return;
}
// construct java task
Java j = new Java(this);
j.setTaskName("verify");
j.setFailonerror(true);
j.setFork(true);
// add classpath for SDK tools
addKitClasses(j);
// set main class
j.setClassname("com.sun.javacard.offcardverifier.Verifier");
// not verbose for now
//j.createArg().setLine("-verbose");
j.createArg().setLine("-nobanner");
// export files for verification
for (File exp : expfiles) {
j.createArg().setFile(exp);
}
// cap file for verification
j.createArg().setFile(project.resolveFile(output_cap));
// report the command
log("command: " + j.getCommandLine(), Project.MSG_VERBOSE);
// perform verification
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
File applet_folder = makeTemp();
// Construct exportpath
ArrayList exps = new ArrayList<>();
exps.add(jckit.getExportDir());
// add imports
for (JCImport imp : raw_imports) {
// Support import clauses with only jar or exp values
if (imp.exps != null) {
File f = new File(imp.exps).getAbsoluteFile();
// Avoid duplicates
if (!exps.contains(f)) {
exps.add(f);
}
}
}
// perform conversion
convert(applet_folder, exps);
// Copy results
if (output_cap != null || output_exp != null || output_jca != null || output_jar != null) {
// 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);
File pkgDir = new File(applet_folder, pkgPath);
File jcsrc = new File(pkgDir, "javacard");
// Interesting paths inside the JC folder
File cap = new File(jcsrc, ln + ".cap");
File exp = new File(jcsrc, ln + ".exp");
File jca = new File(jcsrc, ln + ".jca");
try {
// copy CAP file
setTaskName("cap");
// check that a CAP file got created
if (!cap.exists()) {
throw new BuildException("Can not find CAP in " + jcsrc);
}
// resolve output path
File outCap = project.resolveFile(output_cap);
// perform the copy
Files.copy(cap.toPath(), outCap.toPath(), 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 (!exp.exists()) {
throw new BuildException("Can not find EXP in " + jcsrc);
}
// resolve output directory
File outExp = project.resolveFile(output_exp);
// determine package directories
File outExpPkg = new File(outExp.toString(), pkgPath);
File outExpPkgJc = new File(outExpPkg, "javacard");
// create directories
if (!outExpPkgJc.exists()) {
if (!outExpPkgJc.mkdirs()) {
throw new HelpingBuildException("Could not create directory " + outExpPkgJc);
}
}
// perform the copy
File exp_file = new File(outExpPkgJc, exp.getName());
Files.copy(exp.toPath(), exp_file.toPath(), 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 (!jca.exists()) {
throw new BuildException("Can not find JCA in " + jcsrc);
}
// resolve output path
outCap = project.resolveFile(output_jca);
Files.copy(jca.toPath(), outCap.toPath(), StandardCopyOption.REPLACE_EXISTING);
log("JCA saved to " + outCap.getAbsolutePath(), 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);
jarz.add(jarout);
// create the JAR
jarz.execute();
log("JAR created at " + outJar.getAbsolutePath(), Project.MSG_INFO);
}
} catch (IOException e) {
e.printStackTrace();
throw new BuildException("Can not copy output CAP, EXP or JCA", e);
}
}
if (verify) {
verify(exps);
}
} finally {
cleanTemp();
}
}
private File makeTemp() {
try {
java.nio.file.Path p = Files.createTempDirectory("jccpro");
File fp = p.toFile();
temporary.add(fp);
return fp;
} catch (IOException e) {
throw new RuntimeException("Can not make temporary folder", e);
}
}
private void cleanTemp() {
// Clean temporary files.
for (File f : temporary) {
if (f.exists()) {
rmminusrf(f.toPath());
}
}
}
}
public static class JCImport {
String exps = null;
String jar = null;
public void setExps(String msg) {
exps = msg;
}
public void setJar(String msg) {
jar = msg;
}
}
}