diff --git a/globalplatform.pro b/globalplatform.pro index 781a16b5..dd7fee04 100644 --- a/globalplatform.pro +++ b/globalplatform.pro @@ -1,7 +1,7 @@ -libraryjars /lib/rt.jar -libraryjars /lib/jce.jar --injars target/gp.jar +-injars tool/target/gp.jar -keep public class pro.javacard.gp.GPTool { public static void main(java.lang.String[]); } diff --git a/library/pom.xml b/library/pom.xml new file mode 100644 index 00000000..b4d8340d --- /dev/null +++ b/library/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + + com.github.martinpaljak + gppro + 19.05.16 + + + globalplatformpro + GlobalPlatformPro library + + + + + com.github.martinpaljak + apdu4j-core + 19.05.08 + + + + com.github.martinpaljak + capfile + 19.03.04 + + + + org.slf4j + slf4j-api + 1.7.25 + + + + com.google.code.gson + gson + 2.8.4 + + + + org.bouncycastle + bcpkix-jdk15on + 1.61 + + + + com.payneteasy + ber-tlv + 1.0-9 + + + + org.testng + testng + 6.14.3 + test + + + \ No newline at end of file diff --git a/library/src/main/java/pro/javacard/gp/DMTokenGenerator.java b/library/src/main/java/pro/javacard/gp/DMTokenGenerator.java new file mode 100644 index 00000000..f0e57571 --- /dev/null +++ b/library/src/main/java/pro/javacard/gp/DMTokenGenerator.java @@ -0,0 +1,89 @@ +package pro.javacard.gp; + +import apdu4j.CommandAPDU; +import apdu4j.HexUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.Signature; + +import static pro.javacard.gp.GPSession.INS_DELETE; + +public class DMTokenGenerator { + private static final Logger logger = LoggerFactory.getLogger(DMTokenGenerator.class); + + private static final String defaultAlgorithm = "SHA1withRSA"; + private final String algorithm; + + private PrivateKey key; + private byte[] token; // Token to use + + public DMTokenGenerator(PrivateKey key, String algorithm) { + this.key = key; + this.algorithm = algorithm; + } + + public DMTokenGenerator(PrivateKey key) { + this(key, defaultAlgorithm); + } + + CommandAPDU applyToken(CommandAPDU apdu) throws GeneralSecurityException { + ByteArrayOutputStream newData = new ByteArrayOutputStream(); + try { + newData.write(apdu.getData()); + + if (key == null) { + logger.trace("No private key for token generation provided"); + if (apdu.getINS() != (INS_DELETE & 0xFF)) + newData.write(0); // No token + } else { + if (apdu.getINS() == (INS_DELETE & 0xFF)) { + // See GP 2.3.1 Table 11-23 + logger.trace("Adding tag 0x9E before Delete Token"); + newData.write(0x9E); + } + logger.trace("Using private key for token generation (" + algorithm + ")"); + byte[] token = calculateToken(apdu, key); + newData.write(token.length); + newData.write(token); + } + } catch (IOException e) { + throw new RuntimeException("Could not apply DM token", e); + } + return new CommandAPDU(apdu.getCLA(), apdu.getINS(), apdu.getP1(), apdu.getP2(), newData.toByteArray()); // FIXME: Le handling + } + + private byte[] calculateToken(CommandAPDU apdu, PrivateKey key) throws GeneralSecurityException { + return signData(key, getTokenData(apdu)); + } + + private static byte[] getTokenData(CommandAPDU apdu) { + try { + ByteArrayOutputStream bo = new ByteArrayOutputStream(); + bo.write(apdu.getP1()); + bo.write(apdu.getP2()); + bo.write(apdu.getData().length); // FIXME: length handling for > 255 bytes + bo.write(apdu.getData()); + return bo.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("Could not get P1/P2 or data for token calculation", e); + } + } + + private byte[] signData(PrivateKey privateKey, byte[] apduData) throws GeneralSecurityException { + Signature signer = Signature.getInstance(algorithm); + signer.initSign(privateKey); + signer.update(apduData); + byte[] signature = signer.sign(); + logger.info("Generated DM token: {}" + HexUtils.bin2hex(signature)); + return signature; + } + + public boolean hasKey() { + return key != null; + } +} diff --git a/src/main/java/pro/javacard/gp/GPSessionKeyProvider.java b/library/src/main/java/pro/javacard/gp/GPCardKeys.java similarity index 55% rename from src/main/java/pro/javacard/gp/GPSessionKeyProvider.java rename to library/src/main/java/pro/javacard/gp/GPCardKeys.java index c81c9e3f..88c4da84 100644 --- a/src/main/java/pro/javacard/gp/GPSessionKeyProvider.java +++ b/library/src/main/java/pro/javacard/gp/GPCardKeys.java @@ -24,21 +24,21 @@ // Providers are free to derive session keys based on hardware backed master keys // PlaintextKeys provides card keys, that are ... plaintext (not backed by hardware) -public abstract class GPSessionKeyProvider { +import apdu4j.HexUtils; - // returns true if keys can probably be made - public abstract boolean init(byte[] atr, byte[] cplc, byte[] kinfo); +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.List; - // Any can be null, if N/A for SCP version - public abstract void calculate(int scp, byte[] kdd, byte[] host_challenge, byte[] card_challenge, byte[] ssc) throws GPException; +public abstract class GPCardKeys { - public abstract GPKey getKeyFor(KeyPurpose p); + protected GPSecureChannel scp; public abstract int getID(); public abstract int getVersion(); - // Session keys are used for various purposes + // Keys are used for various purposes public enum KeyPurpose { // ID is as used in diversification/derivation // That is - one based. @@ -53,6 +53,33 @@ public enum KeyPurpose { public byte getValue() { return (byte) (value & 0xFF); } + + // RMAC is derived, but not loaded to the card + public static List cardKeys() { + return Arrays.asList(ENC, MAC, DEK); + } } + // Encrypt data with static card DEK + public abstract byte[] encrypt(byte[] data) throws GeneralSecurityException; + + // Encrypt a key with card (or session) DEK + public abstract byte[] encryptKey(GPCardKeys key, KeyPurpose p) throws GeneralSecurityException; + + // Get session keys for given session data + public abstract GPSessionKeys getSessionKeys(byte[] kdd); + + // Get KCV of a card key + public abstract byte[] kcv(KeyPurpose p); + + // Diversify card keys automatically, based on INITIALIZE UPDATE response + public GPCardKeys diversify(GPSecureChannel scp, byte[] kdd) { + this.scp = scp; + return this; + } + + @Override + public String toString() { + return String.format("KCV-s ENC=%s MAC=%s DEK=%s for %s", HexUtils.bin2hex(kcv(KeyPurpose.ENC)), HexUtils.bin2hex(kcv(KeyPurpose.MAC)), HexUtils.bin2hex(kcv(KeyPurpose.DEK)), scp); + } } diff --git a/library/src/main/java/pro/javacard/gp/GPCardProfile.java b/library/src/main/java/pro/javacard/gp/GPCardProfile.java new file mode 100644 index 00000000..75a845c1 --- /dev/null +++ b/library/src/main/java/pro/javacard/gp/GPCardProfile.java @@ -0,0 +1,4 @@ +package pro.javacard.gp; + +public class GPCardProfile { +} diff --git a/src/main/java/pro/javacard/gp/GPCommands.java b/library/src/main/java/pro/javacard/gp/GPCommands.java similarity index 74% rename from src/main/java/pro/javacard/gp/GPCommands.java rename to library/src/main/java/pro/javacard/gp/GPCommands.java index 027d1191..bff4f830 100644 --- a/src/main/java/pro/javacard/gp/GPCommands.java +++ b/library/src/main/java/pro/javacard/gp/GPCommands.java @@ -19,32 +19,32 @@ */ package pro.javacard.gp; +import apdu4j.CommandAPDU; import apdu4j.HexUtils; +import apdu4j.ResponseAPDU; import pro.javacard.AID; -import javax.smartcardio.CardException; -import javax.smartcardio.CommandAPDU; -import javax.smartcardio.ResponseAPDU; +import java.io.IOException; import java.io.PrintStream; // Middle layer between GPTool (CLI) and GlobalPlatform (session) public class GPCommands { - private static void storeDGI(GlobalPlatform gp, byte[] payload) throws GPException, CardException { + private static void storeDGI(GPSession gp, byte[] payload) throws GPException, IOException { // Single DGI. 0x90 should work as well but 0x80 is actually respected by cards. - CommandAPDU cmd = new CommandAPDU(GlobalPlatform.CLA_GP, GlobalPlatform.INS_STORE_DATA, 0x80, 0x00, payload); + CommandAPDU cmd = new CommandAPDU(GPSession.CLA_GP, GPSession.INS_STORE_DATA, 0x80, 0x00, payload); ResponseAPDU response = gp.transmit(cmd); GPException.check(response, "STORE DATA failed"); } - public static void setPrePerso(GlobalPlatform gp, byte[] data) throws GPException, CardException { + public static void setPrePerso(GPSession gp, byte[] data) throws GPException, IOException { if (data == null || data.length != 8) throw new IllegalArgumentException("PrePerso data must be 8 bytes"); byte[] payload = GPUtils.concatenate(new byte[]{(byte) 0x9f, 0x67, (byte) data.length}, data); storeDGI(gp, payload); } - public static void setPerso(GlobalPlatform gp, byte[] data) throws GPException, CardException { + public static void setPerso(GPSession gp, byte[] data) throws GPException, IOException { if (data == null || data.length != 8) throw new IllegalArgumentException("Perso data must be 8 bytes"); byte[] payload = GPUtils.concatenate(new byte[]{(byte) 0x9f, 0x66, (byte) data.length}, data); @@ -66,11 +66,10 @@ public static void listRegistry(GPRegistry reg, PrintStream out, boolean verbose out.println(tab + "Parent: " + e.getDomain()); } if (e.getType() == GPRegistryEntry.Kind.ExecutableLoadFile) { - GPRegistryEntryPkg pkg = (GPRegistryEntryPkg) e; - if (pkg.getVersion() != null) { - out.println(tab + "Version: " + pkg.getVersionString()); + if (e.getVersion() != null) { + out.println(tab + "Version: " + e.getVersionString()); } - for (AID a : pkg.getModules()) { + for (AID a : e.getModules()) { out.print(tab + "Applet: " + HexUtils.bin2hex(a.getBytes())); if (verbose) { out.println(" (" + GPUtils.byteArrayToReadableString(a.getBytes()) + ")"); @@ -79,12 +78,11 @@ public static void listRegistry(GPRegistry reg, PrintStream out, boolean verbose } } } else { - GPRegistryEntryApp app = (GPRegistryEntryApp) e; - if (app.getLoadFile() != null) { - out.println(tab + "From: " + app.getLoadFile()); + if (e.getLoadFile() != null) { + out.println(tab + "From: " + e.getLoadFile()); } //if (!app.getPrivileges().isEmpty()) { - out.println(tab + "Privs: " + app.getPrivileges()); + out.println(tab + "Privs: " + e.getPrivileges()); //} } out.println(); diff --git a/src/main/java/pro/javacard/gp/GPCrypto.java b/library/src/main/java/pro/javacard/gp/GPCrypto.java similarity index 79% rename from src/main/java/pro/javacard/gp/GPCrypto.java rename to library/src/main/java/pro/javacard/gp/GPCrypto.java index 669447ce..ad4fe9d3 100644 --- a/src/main/java/pro/javacard/gp/GPCrypto.java +++ b/library/src/main/java/pro/javacard/gp/GPCrypto.java @@ -32,15 +32,15 @@ import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; -import pro.javacard.AID; -import pro.javacard.gp.GPKey.Type; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; -import java.io.*; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.cert.CertificateException; @@ -111,13 +111,13 @@ public static void buffer_increment(byte[] buffer) { } // 3des mac - public static byte[] mac_3des(GPKey key, byte[] text, byte[] iv) { + public static byte[] mac_3des(byte[] key, byte[] text, byte[] iv) { byte[] d = pad80(text, 8); - return mac_3des(key.getKeyAs(Type.DES3), d, 0, d.length, iv); + return mac_3des(new SecretKeySpec(resizeDES(key, 24), "DESede"), d, 0, d.length, iv); } - // 3des mac with null iv - public static byte[] mac_3des_nulliv(GPKey key, byte[] d) { + + public static byte[] mac_3des_nulliv(byte[] key, byte[] d) { return mac_3des(key, d, null_bytes_8); } @@ -135,17 +135,17 @@ static byte[] mac_3des(Key key, byte[] text, int offset, int length, byte[] iv) } // The weird mac - public static byte[] mac_des_3des(GPKey key, byte[] text, byte[] iv) { + public static byte[] mac_des_3des(byte[] key, byte[] text, byte[] iv) { byte[] d = pad80(text, 8); return mac_des_3des(key, d, 0, d.length, iv); } - private static byte[] mac_des_3des(GPKey key, byte[] text, int offset, int length, byte[] iv) { + private static byte[] mac_des_3des(byte[] key, byte[] text, int offset, int length, byte[] iv) { try { Cipher cipher1 = Cipher.getInstance(DES_CBC_CIPHER); - cipher1.init(Cipher.ENCRYPT_MODE, key.getKeyAs(Type.DES), new IvParameterSpec(iv)); + cipher1.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(resizeDES(key, 8), "DES"), new IvParameterSpec(iv)); Cipher cipher2 = Cipher.getInstance(DES3_CBC_CIPHER); - cipher2.init(Cipher.ENCRYPT_MODE, key.getKeyAs(Type.DES3), new IvParameterSpec(iv)); + cipher2.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(resizeDES(key, 24), "DESede"), new IvParameterSpec(iv)); byte[] result = new byte[8]; byte[] temp; @@ -153,7 +153,7 @@ private static byte[] mac_des_3des(GPKey key, byte[] text, int offset, int lengt if (length > 8) { temp = cipher1.doFinal(text, offset, length - 8); System.arraycopy(temp, temp.length - 8, result, 0, 8); - cipher2.init(Cipher.ENCRYPT_MODE, key.getKeyAs(Type.DES3), new IvParameterSpec(result)); + cipher2.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(resizeDES(key, 24), "DESede"), new IvParameterSpec(result)); } temp = cipher2.doFinal(text, (offset + length) - 8, 8); System.arraycopy(temp, temp.length - 8, result, 0, 8); @@ -164,10 +164,6 @@ private static byte[] mac_des_3des(GPKey key, byte[] text, int offset, int lengt } // SCP03 related - public static byte[] scp03_mac(GPKey key, byte[] msg, int lengthbits) { - return scp03_mac(key.getBytes(), msg, lengthbits); - } - public static byte[] scp03_mac(byte[] keybytes, byte[] msg, int lengthBits) { // Use BouncyCastle light interface. BlockCipher cipher = new AESEngine(); @@ -180,11 +176,7 @@ public static byte[] scp03_mac(byte[] keybytes, byte[] msg, int lengthBits) { } // GP 2.2.1 Amendment D v 1.1.1 - public static byte[] scp03_kdf(GPKey key, byte constant, byte[] context, int blocklen_bits) { - return scp03_kdf(key.getBytes(), constant, context, blocklen_bits); - } - - private static byte[] scp03_kdf(byte[] key, byte constant, byte[] context, int blocklen_bits) { + static byte[] scp03_kdf(byte[] key, byte constant, byte[] context, int blocklen_bits) { // 11 bytes byte[] label = new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; @@ -216,45 +208,49 @@ public static byte[] scp03_kdf(byte[] key, byte[] a, byte[] b, int bytes) { // GPC 2.2.1 Amendment D 7.2.2 - public static byte[] scp03_key_check_value(GPKey key) { + public static byte[] kcv_aes(byte[] key) { try { Cipher c = Cipher.getInstance(AES_CBC_CIPHER); - c.init(Cipher.ENCRYPT_MODE, key.getKeyAs(Type.AES), iv_null_16); + c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), iv_null_16); byte[] cv = c.doFinal(one_bytes_16); return Arrays.copyOfRange(cv, 0, 3); } catch (GeneralSecurityException e) { - throw new RuntimeException("Could not calculate KCV", e); - } - } - - public static byte[] scp03_encrypt_key(GPKey dek, GPKey key) { - try { - // Pad with random - int n = key.getLength() % 16 + 1; - byte[] plaintext = new byte[n * key.getLength()]; - random.nextBytes(plaintext); - System.arraycopy(key.getBytes(), 0, plaintext, 0, key.getLength()); - // encrypt - Cipher c = Cipher.getInstance(AES_CBC_CIPHER); - c.init(Cipher.ENCRYPT_MODE, dek.getKeyAs(Type.AES), iv_null_16); - byte[] cgram = c.doFinal(plaintext); - return cgram; - } catch (GeneralSecurityException e) { - throw new RuntimeException("Could not encrypt key", e); + throw new GPException("Could not calculate KCV", e); } } - public static byte[] kcv_3des(GPKey key) { + public static byte[] kcv_3des(byte[] key) { try { Cipher cipher = Cipher.getInstance(DES3_ECB_CIPHER); - cipher.init(Cipher.ENCRYPT_MODE, key.getKeyAs(Type.DES3)); + cipher.init(Cipher.ENCRYPT_MODE, des3key(key)); byte check[] = cipher.doFinal(GPCrypto.null_bytes_8); return Arrays.copyOf(check, 3); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { - throw new RuntimeException("Could not calculate KCV", e); + } catch (GeneralSecurityException e) { + throw new GPException("Could not calculate KCV", e); } } + static Key des3key(byte[] v) { + return new SecretKeySpec(resizeDES(v, 24), "DESede"); + } + + static Key aeskey(byte[] v) { + return new SecretKeySpec(v, "AES"); + } + + public static byte[] dek_encrypt_des(byte[] key, byte[] data) throws GeneralSecurityException { + Cipher cipher = Cipher.getInstance(DES3_ECB_CIPHER); + cipher.init(Cipher.ENCRYPT_MODE, des3key(key)); + return cipher.doFinal(data); + } + + public static byte[] dek_encrypt_aes(byte[] key, byte[] data) throws GeneralSecurityException { + Cipher cipher = Cipher.getInstance(AES_CBC_CIPHER); + cipher.init(Cipher.ENCRYPT_MODE, aeskey(key), iv_null_16); + return cipher.doFinal(data); + } + + // Get a public key from a PEM file, either public key or keypair public static PublicKey pem2PublicKey(InputStream in) throws IOException { try (PEMParser pem = new PEMParser(new InputStreamReader(in, StandardCharsets.US_ASCII))) { @@ -288,4 +284,17 @@ public static PrivateKey pem2PrivateKey(InputStream in) throws IOException { } } + // Do shuffling as necessary + static byte[] resizeDES(byte[] key, int length) { + if (length == 24) { + byte[] key24 = new byte[24]; + System.arraycopy(key, 0, key24, 0, 16); + System.arraycopy(key, 0, key24, 16, 8); + return key24; + } else { + byte[] key8 = new byte[8]; + System.arraycopy(key, 0, key8, 0, 8); + return key8; + } + } } diff --git a/src/main/java/pro/javacard/gp/GPData.java b/library/src/main/java/pro/javacard/gp/GPData.java similarity index 69% rename from src/main/java/pro/javacard/gp/GPData.java rename to library/src/main/java/pro/javacard/gp/GPData.java index 2e6a20bc..217d2146 100644 --- a/src/main/java/pro/javacard/gp/GPData.java +++ b/library/src/main/java/pro/javacard/gp/GPData.java @@ -19,26 +19,25 @@ */ package pro.javacard.gp; +import apdu4j.APDUBIBO; +import apdu4j.CommandAPDU; import apdu4j.HexUtils; -import apdu4j.ISO7816; -import com.payneteasy.tlv.*; +import apdu4j.ResponseAPDU; +import com.payneteasy.tlv.BerTag; +import com.payneteasy.tlv.BerTlv; +import com.payneteasy.tlv.BerTlvParser; +import com.payneteasy.tlv.BerTlvs; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import pro.javacard.gp.GPKey.Type; -import javax.smartcardio.CardChannel; -import javax.smartcardio.CardException; -import javax.smartcardio.CommandAPDU; -import javax.smartcardio.ResponseAPDU; import java.io.IOException; -import java.io.PrintStream; import java.math.BigInteger; import java.text.SimpleDateFormat; import java.util.*; import java.util.stream.Collectors; -import static pro.javacard.gp.GlobalPlatform.CLA_GP; +import static pro.javacard.gp.GPSession.CLA_GP; // Various constants from GP specification and other sources // Methods to pretty-print those structures and constants. @@ -51,17 +50,7 @@ public final class GPData { public static final byte securedStatus = 0xF; public static final byte lockedStatus = 0x7F; public static final byte terminatedStatus = (byte) 0xFF; - // See GP 2.1.1 Table 9-7: Application Privileges - @Deprecated - public static final byte defaultSelectedPriv = 0x04; - @Deprecated - public static final byte cardLockPriv = 0x10; - @Deprecated - public static final byte cardTerminatePriv = 0x08; - @Deprecated - public static final byte securityDomainPriv = (byte) 0x80; - // Default test key TODO: provide getters for arrays, this class should be kept public - static final byte[] defaultKeyBytes = HexUtils.hex2bin("404142434445464748494A4B4C4D4E4F"); + // Default ISD AID-s static final byte[] defaultISDBytes = HexUtils.hex2bin("A000000151000000"); static final Map sw = new HashMap<>(); @@ -90,138 +79,6 @@ public final class GPData { sw.put(0x6A88, "Referenced data not found"); // 2.3 Table 11-78 } - // GP 2.1.1 9.1.6 - // GP 2.2.1 11.1.8 - public static String get_key_type_coding_string(int type) { - if ((0x00 <= type) && (type <= 0x7f)) - return "Reserved for private use"; - // symmetric - if (0x80 == type) - return "DES - mode (ECB/CBC) implicitly known"; - if (0x81 == type) - return "Reserved (Triple DES)"; - if (0x82 == type) - return "Triple DES in CBC mode"; - if (0x83 == type) - return "DES in ECB mode"; - if (0x84 == type) - return "DES in CBC mode"; - if (0x85 == type) - return "Pre-Shared Key for Transport Layer Security"; - if (0x88 == type) - return "AES (16, 24, or 32 long keys)"; - if (0x90 == type) - return "HMAC-SHA1 - length of HMAC is implicitly known"; - if (0x91 == type) - return "MAC-SHA1-160 - length of HMAC is 160 bits"; - if (type == 0x86 || type == 0x87 || ((0x89 <= type) && (type <= 0x8F)) || ((0x92 <= type) && (type <= 0x9F))) - return "RFU (asymmetric algorithms)"; - // asymmetric - if (0xA0 == type) - return "RSA Public Key - public exponent e component (clear text)"; - if (0xA1 == type) - return "RSA Public Key - modulus N component (clear text)"; - if (0xA2 == type) - return "RSA Private Key - modulus N component"; - if (0xA3 == type) - return "RSA Private Key - private exponent d component"; - if (0xA4 == type) - return "RSA Private Key - Chinese Remainder P component"; - if (0xA5 == type) - return "RSA Private Key - Chinese Remainder Q component"; - if (0xA6 == type) - return "RSA Private Key - Chinese Remainder PQ component"; - if (0xA7 == type) - return "RSA Private Key - Chinese Remainder DP1 component"; - if (0xA8 == type) - return "RSA Private Key - Chinese Remainder DQ1 component"; - if ((0xA9 <= type) && (type <= 0xFE)) - return "RFU (asymmetric algorithms)"; - if (0xFF == type) - return "Extended Format"; - - return "UNKNOWN"; - } - - public static GPKey getDefaultKey() { - return new GPKey(defaultKeyBytes, Type.DES3); - } - - // Print the key template - public static void pretty_print_key_template(List list, PrintStream out) { - boolean factory_keys = false; - out.flush(); - for (GPKey k : list) { - // Descriptive text about the key - final String nice; - if (k.getType() == Type.RSAPUB && k.getLength() > 0) { - nice = "(RSA-" + k.getLength() * 8 + " public)"; - } else if (k.getType() == Type.AES && k.getLength() > 0) { - nice = "(AES-" + k.getLength() * 8 + ")"; - } else { - nice = ""; - } - - // Detect unaddressable factory keys - if (k.getVersion() == 0x00 || k.getVersion() == 0xFF) - factory_keys = true; - - // print - out.println(String.format("Version: %3d (0x%02X) ID: %3d (0x%02X) type: %-4s length: %3d %s", k.getVersion(), k.getVersion(), k.getID(), k.getID(), k.getType(), k.getLength(), nice)); - } - if (factory_keys) { - out.println("Key version suggests factory keys"); - } - out.flush(); - } - - // GP 2.1.1 9.3.3.1 - // GP 2.2.1 11.3.3.1 and 11.1.8 - // TODO: move to GPKey - public static List get_key_template_list(byte[] data) throws GPException { - List r = new ArrayList<>(); - - BerTlvParser parser = new BerTlvParser(); - BerTlvs tlvs = parser.parse(data); - GPUtils.trace_tlv(data, logger); - - BerTlv keys = tlvs.find(new BerTag(0xE0)); - if (keys != null && keys.isConstructed()) { - for (BerTlv key : keys.findAll(new BerTag(0xC0))) { - byte[] tmpl = key.getBytesValue(); - if (tmpl.length == 0) { - // Fresh SSD with an empty template. - logger.info("Key template has zero length (empty). Skipping."); - continue; - } - if (tmpl.length < 4) { - throw new GPDataException("Key info template shorter than 4 bytes", tmpl); - } - int offset = 0; - int id = tmpl[offset++] & 0xFF; - int version = tmpl[offset++] & 0xFF; - int type = tmpl[offset++] & 0xFF; - boolean extended = type == 0xFF; - if (extended) { - // extended key type, use second byte - type = tmpl[offset++] & 0xFF; - } - // parse length - int length = tmpl[offset++] & 0xFF; - if (extended) { - length = length << 8 | tmpl[offset++] & 0xFF; - } - if (extended) { - // XXX usage and access is not shown currently - logger.warn("Extended format not parsed: " + HexUtils.bin2hex(Arrays.copyOfRange(tmpl, tmpl.length - 4, tmpl.length))); - } - // XXX: RSAPUB keys have two components A1 and A0, gets called with A1 and A0 (exponent) discarded - r.add(new GPKey(version, id, length, type)); - } - } - return r; - } - // GP 2.1.1: F.2 Table F-1 // Tag 66 with nested 73 public static void pretty_print_card_data(byte[] data) { @@ -290,23 +147,28 @@ public static void pretty_print_card_capabilities(byte[] data) throws GPDataExce for (BerTlv v : caps.getValues()) { BerTlv t = v.find(new BerTag(0xA0)); if (t != null) { - int scp = t.find(new BerTag(0x80)).getIntValue(); - byte[] is = t.find(new BerTag(0x81)).getBytesValue(); - System.out.format("Supports: SCP%02X", scp); - for (int i = 0; i < is.length; i++) { - System.out.format(" i=%02X", is[i]); - } - BerTlv keylens = t.find(new BerTag(0x82)); - if (keylens != null) { - System.out.print(" with"); - if ((keylens.getIntValue() & 0x01) == 0x01) { - System.out.print(" AES-128"); - } - if ((keylens.getIntValue() & 0x02) == 0x02) { - System.out.print(" AES-196"); + BerTlv scp = t.find(new BerTag(0x80)); + if (scp != null) { + System.out.format("Supports: SCP%02X", scp.getIntValue()); + BerTlv is = t.find(new BerTag(0x81)); + if (is != null) { + byte[] isv = is.getBytesValue(); + for (int i = 0; i < isv.length; i++) { + System.out.format(" i=%02X", isv[i]); + } } - if ((keylens.getIntValue() & 0x04) == 0x04) { - System.out.print(" AES-256"); + BerTlv keylens = t.find(new BerTag(0x82)); + if (keylens != null) { + System.out.print(" with"); + if ((keylens.getIntValue() & 0x01) == 0x01) { + System.out.print(" AES-128"); + } + if ((keylens.getIntValue() & 0x02) == 0x02) { + System.out.print(" AES-196"); + } + if ((keylens.getIntValue() & 0x04) == 0x04) { + System.out.print(" AES-256"); + } } } System.out.println(); @@ -354,7 +216,7 @@ public static void pretty_print_card_capabilities(byte[] data) throws GPDataExce // NB! This assumes a selected (I)SD! - public static void dump(CardChannel channel) throws CardException, GPException { + public static void dump(APDUBIBO channel) throws IOException, GPException { byte[] cplc = fetchCPLC(channel); if (cplc != null) { System.out.println(GPData.CPLC.fromBytes(cplc).toPrettyString()); @@ -396,17 +258,16 @@ public static void dump(CardChannel channel) throws CardException, GPException { // Print Key Info Template byte[] keyInfo = fetchKeyInfoTemplate(channel); if (keyInfo != null) { - pretty_print_key_template(GPData.get_key_template_list(keyInfo), System.out); + GPKeyInfo.print(GPKeyInfo.parseTemplate(keyInfo), System.out); } } - // Just to encapsulate tag constants behind meaningful name - public static byte[] fetchCPLC(CardChannel channel) throws CardException { + public static byte[] fetchCPLC(APDUBIBO channel) throws IOException { return getData(channel, 0x9f, 0x7f, "CPLC", true); } - public static byte[] fetchKeyInfoTemplate(CardChannel channel) throws CardException { + public static byte[] fetchKeyInfoTemplate(APDUBIBO channel) throws IOException { return getData(channel, 0x00, 0xE0, "Key Info Template", false); } @@ -434,20 +295,20 @@ public static String oid2string(byte[] oid) { } } - public static GlobalPlatform.GPSpec oid2version(byte[] bytes) throws GPDataException { + public static GPSession.GPSpec oid2version(byte[] bytes) throws GPDataException { String oid = oid2string(bytes); if (oid.equals("1.2.840.114283.2.2.1.1")) { - return GlobalPlatform.GPSpec.GP211; + return GPSession.GPSpec.GP211; } else if (oid.equals("1.2.840.114283.2.2.2")) { - return GlobalPlatform.GPSpec.GP22; + return GPSession.GPSpec.GP22; } else if (oid.equals("1.2.840.114283.2.2.2.1")) { - return GlobalPlatform.GPSpec.GP22; // No need to make a difference + return GPSession.GPSpec.GP22; // No need to make a difference } else { throw new GPDataException("Unknown GP version OID: " + oid, bytes); } } - public static byte[] getData(CardChannel channel, int p1, int p2, String name, boolean failsafe) throws CardException { + public static byte[] getData(APDUBIBO channel, int p1, int p2, String name, boolean failsafe) throws IOException { logger.trace("GET DATA({})", name); ResponseAPDU resp = channel.transmit(new CommandAPDU(CLA_GP, ISO7816.INS_GET_DATA, p1, p2, 256)); if (failsafe && resp.getSW() != ISO7816.SW_NO_ERROR) diff --git a/src/main/java/pro/javacard/gp/GPDataException.java b/library/src/main/java/pro/javacard/gp/GPDataException.java similarity index 93% rename from src/main/java/pro/javacard/gp/GPDataException.java rename to library/src/main/java/pro/javacard/gp/GPDataException.java index fc6c063e..f03be287 100644 --- a/src/main/java/pro/javacard/gp/GPDataException.java +++ b/library/src/main/java/pro/javacard/gp/GPDataException.java @@ -21,6 +21,7 @@ import apdu4j.HexUtils; +// Thrown when some data is not parseable for some reason, and includes the data in question. @SuppressWarnings("serial") public class GPDataException extends GPException { diff --git a/src/main/java/pro/javacard/gp/GPException.java b/library/src/main/java/pro/javacard/gp/GPException.java similarity index 96% rename from src/main/java/pro/javacard/gp/GPException.java rename to library/src/main/java/pro/javacard/gp/GPException.java index bd709ceb..8bd3602f 100644 --- a/src/main/java/pro/javacard/gp/GPException.java +++ b/library/src/main/java/pro/javacard/gp/GPException.java @@ -23,13 +23,13 @@ package pro.javacard.gp; -import javax.smartcardio.ResponseAPDU; +import apdu4j.ResponseAPDU; /** * Root exception class for all global platform protocol errors. */ @SuppressWarnings("serial") -public class GPException extends Exception { +public class GPException extends RuntimeException { /** * Response status indicating the error, or 0 if not applicable. diff --git a/library/src/main/java/pro/javacard/gp/GPKeyInfo.java b/library/src/main/java/pro/javacard/gp/GPKeyInfo.java new file mode 100644 index 00000000..e51eeb40 --- /dev/null +++ b/library/src/main/java/pro/javacard/gp/GPKeyInfo.java @@ -0,0 +1,232 @@ +/* + * GlobalPlatformPro - GlobalPlatform tool + * + * Copyright (C) 2015-2017 Martin Paljak, martin@martinpaljak.net + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package pro.javacard.gp; + +import apdu4j.HexUtils; +import com.payneteasy.tlv.BerTag; +import com.payneteasy.tlv.BerTlv; +import com.payneteasy.tlv.BerTlvParser; +import com.payneteasy.tlv.BerTlvs; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +// Encapsulates key metadata +public final class GPKeyInfo { + private static final Logger logger = LoggerFactory.getLogger(GPKeyInfo.class); + + private Type type; + private int version = 0; // 1..7f + private int id = -1; // 0..7f + private int length = -1; + + // Called when parsing KeyInfo template + private GPKeyInfo(int version, int id, int length, int type) { + this.version = version; + this.id = id; + this.length = length; + // FIXME: these values should be encapsulated somewhere + // FIXME: 0x81 is actually reserved according to GP + // GP 2.2.1 11.1.8 Key Type Coding + if (type == 0x80 || type == 0x81 || type == 0x82) { + this.type = Type.DES3; + } else if (type == 0x88) { + this.type = Type.AES; + } else if (type == 0xA1 || type == 0xA0) { + this.type = Type.RSAPUB; + } else if (type == 0x85) { + this.type = Type.PSK; + } else { + throw new UnsupportedOperationException(String.format("Only AES, 3DES, PSK and RSA public keys are supported currently: 0x%02X", type)); + } + } + + // GP 2.1.1 9.3.3.1 + // GP 2.2.1 11.3.3.1 and 11.1.8 + public static List parseTemplate(byte[] data) throws GPException { + List r = new ArrayList<>(); + if (data == null || data.length == 0) + return r; + + BerTlvParser parser = new BerTlvParser(); + BerTlvs tlvs = parser.parse(data); + GPUtils.trace_tlv(data, logger); + + BerTlv keys = tlvs.find(new BerTag(0xE0)); + if (keys != null && keys.isConstructed()) { + for (BerTlv key : keys.findAll(new BerTag(0xC0))) { + byte[] tmpl = key.getBytesValue(); + if (tmpl.length == 0) { + // Fresh SSD with an empty template. + logger.info("Key template has zero length (empty). Skipping."); + continue; + } + if (tmpl.length < 4) { + throw new GPDataException("Key info template shorter than 4 bytes", tmpl); + } + int offset = 0; + int id = tmpl[offset++] & 0xFF; + int version = tmpl[offset++] & 0xFF; + int type = tmpl[offset++] & 0xFF; + boolean extended = type == 0xFF; + if (extended) { + // extended key type, use second byte + type = tmpl[offset++] & 0xFF; + } + // parse length + int length = tmpl[offset++] & 0xFF; + if (extended) { + length = length << 8 | tmpl[offset++] & 0xFF; + } + if (extended) { + // XXX usage and access is not shown currently + logger.warn("Extended format not parsed: " + HexUtils.bin2hex(Arrays.copyOfRange(tmpl, tmpl.length - 4, tmpl.length))); + } + // XXX: RSAPUB keys have two components A1 and A0, gets called with A1 and A0 (exponent) discarded + r.add(new GPKeyInfo(version, id, length, type)); + } + } + return r; + } + + // Print the key template + public static void print(List list, PrintStream out) { + boolean factory_keys = false; + out.flush(); + for (GPKeyInfo k : list) { + // Descriptive text about the key + final String nice; + if (k.getType() == Type.RSAPUB && k.getLength() > 0) { + nice = "(RSA-" + k.getLength() * 8 + " public)"; + } else if (k.getType() == Type.AES && k.getLength() > 0) { + nice = "(AES-" + k.getLength() * 8 + ")"; + } else { + nice = ""; + } + + // Detect unaddressable factory keys + if (k.getVersion() == 0x00 || k.getVersion() == 0xFF) + factory_keys = true; + + // print + out.println(String.format("Version: %3d (0x%02X) ID: %3d (0x%02X) type: %-4s length: %3d %s", k.getVersion(), k.getVersion(), k.getID(), k.getID(), k.getType(), k.getLength(), nice)); + } + if (factory_keys) { + out.println("Key version suggests factory keys"); + } + out.flush(); + } + + public int getID() { + return id; + } + + public int getVersion() { + return version; + } + + public int getLength() { + return length; + } + + public Type getType() { + return type; + } + + public String toString() { + StringBuffer s = new StringBuffer(); + s.append("type=" + type); + if (version >= 1 && version <= 0x7f) + s.append(" version=" + String.format("%d (0x%02X)", version, version)); + if (id >= 0 && id <= 0x7F) + s.append(" id=" + String.format("%d (0x%02X)", id, id)); + s.append(" len=" + length); + return s.toString(); + } + + public enum Type { + DES3, AES, RSAPUB, PSK; + + @Override + public String toString() { + if (this.name().equals("RSAPUB")) + return "RSA"; + return super.toString(); + } + } + + + // GP 2.1.1 9.1.6 + // GP 2.2.1 11.1.8 + public static String type2str(int type) { + if ((0x00 <= type) && (type <= 0x7f)) + return "Reserved for private use"; + // symmetric + if (0x80 == type) + return "DES - mode (ECB/CBC) implicitly known"; + if (0x81 == type) + return "Reserved (Triple DES)"; + if (0x82 == type) + return "Triple DES in CBC mode"; + if (0x83 == type) + return "DES in ECB mode"; + if (0x84 == type) + return "DES in CBC mode"; + if (0x85 == type) + return "Pre-Shared Key for Transport Layer Security"; + if (0x88 == type) + return "AES (16, 24, or 32 long keys)"; + if (0x90 == type) + return "HMAC-SHA1 - length of HMAC is implicitly known"; + if (0x91 == type) + return "MAC-SHA1-160 - length of HMAC is 160 bits"; + if (type == 0x86 || type == 0x87 || ((0x89 <= type) && (type <= 0x8F)) || ((0x92 <= type) && (type <= 0x9F))) + return "RFU (asymmetric algorithms)"; + // asymmetric + if (0xA0 == type) + return "RSA Public Key - public exponent e component (clear text)"; + if (0xA1 == type) + return "RSA Public Key - modulus N component (clear text)"; + if (0xA2 == type) + return "RSA Private Key - modulus N component"; + if (0xA3 == type) + return "RSA Private Key - private exponent d component"; + if (0xA4 == type) + return "RSA Private Key - Chinese Remainder P component"; + if (0xA5 == type) + return "RSA Private Key - Chinese Remainder Q component"; + if (0xA6 == type) + return "RSA Private Key - Chinese Remainder PQ component"; + if (0xA7 == type) + return "RSA Private Key - Chinese Remainder DP1 component"; + if (0xA8 == type) + return "RSA Private Key - Chinese Remainder DQ1 component"; + if ((0xA9 <= type) && (type <= 0xFE)) + return "RFU (asymmetric algorithms)"; + if (0xFF == type) + return "Extended Format"; + + return "UNKNOWN"; + } +} diff --git a/library/src/main/java/pro/javacard/gp/GPRegistry.java b/library/src/main/java/pro/javacard/gp/GPRegistry.java new file mode 100644 index 00000000..696127ff --- /dev/null +++ b/library/src/main/java/pro/javacard/gp/GPRegistry.java @@ -0,0 +1,216 @@ +/* + * gpj - Global Platform for Java SmartCardIO + * + * Copyright (C) 2009 Wojciech Mostowski, woj@cs.ru.nl + * Copyright (C) 2009 Francois Kooman, F.Kooman@student.science.ru.nl + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +package pro.javacard.gp; + +import com.payneteasy.tlv.BerTag; +import com.payneteasy.tlv.BerTlv; +import com.payneteasy.tlv.BerTlvParser; +import com.payneteasy.tlv.BerTlvs; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import pro.javacard.AID; +import pro.javacard.gp.GPRegistryEntry.Kind; +import pro.javacard.gp.GPRegistryEntry.Privilege; +import pro.javacard.gp.GPRegistryEntry.Privileges; +import pro.javacard.gp.GPSession.GPSpec; + +import java.util.*; +import java.util.function.BinaryOperator; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class GPRegistry implements Iterable { + private static final Logger logger = LoggerFactory.getLogger(GPRegistry.class); + boolean tags = true; // XXX (visibility) true if newer tags format should be used for parsing, false otherwise + ArrayList entries = new ArrayList<>(); + + public void add(GPRegistryEntry entry) { + // "fix" the kind at a single location. + if (entry.getPrivileges().has(Privilege.SecurityDomain) && entry.getType() == Kind.Application) { + entry.setType(Kind.SecurityDomain); + } + entries.add(entry); + } + + public Iterator iterator() { + return entries.iterator(); + } + + public List allPackages() { + return entries.stream().filter(e -> e.isPackage()).collect(Collectors.toList()); + } + + public List allPackageAIDs() { + return allPackages().stream().map(e -> e.getAID()).collect(Collectors.toList()); + } + + public List allAppletAIDs() { + return allApplets().stream().map(e -> e.getAID()).collect(Collectors.toList()); + } + + public List allAIDs() { + return entries.stream().map(e -> e.getAID()).collect(Collectors.toList()); + } + + public Optional getDomain(AID aid) { + return allDomains().stream().filter(e -> e.aid.equals(aid)).reduce(onlyOne()); + } + + public List allApplets() { + return entries.stream().filter(e -> e.isApplet()).collect(Collectors.toList()); + } + + public List allDomains() { + return entries.stream().filter(e -> e.isDomain()).collect(Collectors.toList()); + } + + public Optional getDefaultSelectedAID() { + return allApplets().stream().filter(e -> e.hasPrivilege(Privilege.CardReset)).map(e -> e.getAID()).reduce(onlyOne()); + } + + public Optional getDefaultSelectedPackageAID() { + Optional defaultAID = getDefaultSelectedAID(); + if (defaultAID.isPresent()) { + return allPackages().stream().filter(e -> e.getModules().contains(defaultAID.get())).map(e -> e.getAID()).reduce(onlyOne()); + } + return defaultAID; + } + + // Shorthand + public Optional getISD() { + // Could be empty if registry is a view from SSD + return allDomains().stream().filter(e -> e.getType() == Kind.IssuerSecurityDomain).reduce(onlyOne()); + } + + private void populate_legacy(int p1, byte[] data, Kind type, GPSpec spec) throws GPDataException { + int offset = 0; + try { + while (offset < data.length) { + int len = data[offset++]; + AID aid = new AID(data, offset, len); + offset += len; + int lifecycle = (data[offset++] & 0xFF); + byte privileges = data[offset++]; + GPRegistryEntry e = new GPRegistryEntry(); + + if (type == Kind.IssuerSecurityDomain || type == Kind.Application) { + e.setType(type); + e.setAID(aid); + e.setPrivileges(Privileges.fromByte(privileges)); + e.setLifeCycle(lifecycle); + } else if (type == Kind.ExecutableLoadFile) { + if (privileges != 0x00) { + throw new GPDataException("Privileges of Load File is not 0x00"); + } + e.setAID(aid); + e.setLifeCycle(lifecycle); + e.setType(type); + // Modules TODO: remove + if (spec != GPSpec.OP201 && p1 != 0x20) { + int num = data[offset++]; + for (int i = 0; i < num; i++) { + len = data[offset++] & 0xFF; + aid = new AID(data, offset, len); + offset += len; + e.addModule(aid); + } + } + } + add(e); + } + } catch (ArrayIndexOutOfBoundsException e) { + throw new GPDataException("Invalid response to GET STATUS", e); + } + } + + private void populate_tags(byte[] data, Kind type) throws GPDataException { + + BerTlvParser parser = new BerTlvParser(); + BerTlvs tlvs = parser.parse(data); + GPUtils.trace_tlv(data, logger); + + for (BerTlv t : tlvs.findAll(new BerTag(0xE3))) { + GPRegistryEntry e = new GPRegistryEntry(); + if (t.isConstructed()) { + BerTlv aid = t.find(new BerTag(0x4f)); + if (aid != null) { + AID aidv = new AID(aid.getBytesValue()); + e.setAID(aidv); + } + BerTlv lifecycletag = t.find(new BerTag(0x9F, 0x70)); + if (lifecycletag != null) { + e.setLifeCycle(lifecycletag.getBytesValue()[0] & 0xFF); + } + + BerTlv privstag = t.find(new BerTag(0xC5)); + if (privstag != null) { + Privileges privs = Privileges.fromBytes(privstag.getBytesValue()); + e.setPrivileges(privs); + } + for (BerTlv cf : t.findAll(new BerTag(0xCF))) { + logger.debug("CF=" + cf.getHexValue() + " for " + e.aid); + // FIXME: how to expose? + } + + BerTlv loadfiletag = t.find(new BerTag(0xC4)); + if (loadfiletag != null) { + e.setLoadFile(new AID(loadfiletag.getBytesValue())); + } + BerTlv versiontag = t.find(new BerTag(0xCE)); + if (versiontag != null) { + e.setVersion(versiontag.getBytesValue()); + } + + for (BerTlv lf : t.findAll(new BerTag(0x84))) { + e.addModule(new AID(lf.getBytesValue())); + } + + BerTlv domaintag = t.find(new BerTag(0xCC)); + if (domaintag != null) { + e.setDomain(new AID(domaintag.getBytesValue())); + } + } + e.setType(type); + add(e); + } + } + + // FIXME: this is ugly + public void parse(int p1, byte[] data, Kind type, GPSpec spec) throws GPDataException { + if (tags) { + populate_tags(data, type); + } else { + populate_legacy(p1, data, type, spec); + } + } + + public static BinaryOperator onlyOne() { + return onlyOne(() -> new GPException("Expected only one ")); + } + + public static BinaryOperator onlyOne(Supplier exception) { + return (e, o) -> { + throw exception.get(); + }; + } +} diff --git a/src/main/java/pro/javacard/gp/GPRegistryEntry.java b/library/src/main/java/pro/javacard/gp/GPRegistryEntry.java similarity index 87% rename from src/main/java/pro/javacard/gp/GPRegistryEntry.java rename to library/src/main/java/pro/javacard/gp/GPRegistryEntry.java index c4beb4e9..0a90b397 100644 --- a/src/main/java/pro/javacard/gp/GPRegistryEntry.java +++ b/library/src/main/java/pro/javacard/gp/GPRegistryEntry.java @@ -22,25 +22,89 @@ import apdu4j.HexUtils; import pro.javacard.AID; +import java.util.ArrayList; +import java.util.Arrays; import java.util.EnumSet; +import java.util.List; import java.util.stream.Collectors; public class GPRegistryEntry { - protected AID aid; - protected int lifecycle; - protected Kind kind; - protected AID domain; // Associated security domain + AID aid; + int lifecycle; + Kind kind; + AID domain; // Associated security domain + + // Apps and Domains + private Privileges privileges = new Privileges(); + private AID loadfile; // source + + // Packages + private byte[] version; + private List modules = new ArrayList<>(); + + + public Privileges getPrivileges() { + return privileges; + } + + void setPrivileges(Privileges privs) { + privileges = privs; + } + + public AID getLoadFile() { + return loadfile; + } + + public void setLoadFile(AID aid) { + this.loadfile = aid; + } + + + public boolean hasPrivilege(Privilege p) { + return privileges.has(p); + } + + public byte[] getVersion() { + if (version == null) + return null; + return version.clone(); + } + + void setVersion(byte[] v) { + version = v.clone(); + } + + public String getVersionString() { + if (version == null) { + return ""; + } + if (version.length == 2) { + return version[0] + "." + version[1]; + } + return ""; + } + + public void addModule(AID aid) { + modules.add(aid); + } + + public List getModules() { + List r = new ArrayList<>(); + r.addAll(modules); + return r; + } + static String getLifeCycleString(Kind kind, int lifeCycleState) { switch (kind) { case IssuerSecurityDomain: switch (lifeCycleState) { - case 0x1: + case 0x01: return "OP_READY"; - case 0x7: + case 0x07: return "INITIALIZED"; - case 0xF: + case 0x0F: return "SECURED"; case 0x7F: return "CARD_LOCKED"; @@ -136,9 +200,7 @@ void setDomain(AID dom) { } public String toString() { - StringBuffer result = new StringBuffer(); - result.append("AID: " + aid + ", " + lifecycle + ", Kind: " + kind.toShortString()); - return result.toString(); + return String.format("%s: %s, %s", kind.toShortString(), HexUtils.bin2hex(aid.getBytes()), lifecycle); } public String getLifeCycleString() { @@ -202,9 +264,7 @@ public static class Privileges { public static Privileges set(Privilege... privs) { Privileges p = new Privileges(); - for (Privilege pv : privs) { - p.add(pv); - } + Arrays.stream(privs).forEach(v -> p.add(v)); return p; } diff --git a/library/src/main/java/pro/javacard/gp/GPSecureChannel.java b/library/src/main/java/pro/javacard/gp/GPSecureChannel.java new file mode 100644 index 00000000..7686cbc0 --- /dev/null +++ b/library/src/main/java/pro/javacard/gp/GPSecureChannel.java @@ -0,0 +1,25 @@ +package pro.javacard.gp; + +import java.util.Optional; + +public enum GPSecureChannel { + SCP01(1), SCP02(2), SCP03(3); + + private final int value; + + GPSecureChannel(int value) { + this.value = value; + } + + public byte getValue() { + return (byte) (value & 0xFF); + } + + public static Optional valueOf(int i) { + for (GPSecureChannel v : values()) + if (v.value == i) + return Optional.of(v); + + return Optional.empty(); + } +} diff --git a/src/main/java/pro/javacard/gp/GlobalPlatform.java b/library/src/main/java/pro/javacard/gp/GPSession.java similarity index 75% rename from src/main/java/pro/javacard/gp/GlobalPlatform.java rename to library/src/main/java/pro/javacard/gp/GPSession.java index 430f213d..3234f240 100644 --- a/src/main/java/pro/javacard/gp/GlobalPlatform.java +++ b/library/src/main/java/pro/javacard/gp/GPSession.java @@ -23,8 +23,10 @@ package pro.javacard.gp; +import apdu4j.APDUBIBO; +import apdu4j.CommandAPDU; import apdu4j.HexUtils; -import apdu4j.ISO7816; +import apdu4j.ResponseAPDU; import com.payneteasy.tlv.BerTag; import com.payneteasy.tlv.BerTlv; import com.payneteasy.tlv.BerTlvParser; @@ -33,29 +35,27 @@ import org.slf4j.LoggerFactory; import pro.javacard.AID; import pro.javacard.CAPFile; -import pro.javacard.gp.GPKey.Type; import pro.javacard.gp.GPRegistryEntry.Kind; import pro.javacard.gp.GPRegistryEntry.Privilege; import pro.javacard.gp.GPRegistryEntry.Privileges; -import javax.crypto.Cipher; -import javax.smartcardio.*; import java.io.*; import java.math.BigInteger; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.PrivateKey; import java.security.interfaces.RSAPublicKey; import java.util.*; +import static pro.javacard.gp.GPCardKeys.KeyPurpose; + /** * Represents a connection to a GlobalPlatform Card (BIBO interface) * Does secure channel and low-level translation of GP* objects to APDU-s and arguments * NOT thread-safe */ -public class GlobalPlatform extends CardChannel implements AutoCloseable { - private static final Logger logger = LoggerFactory.getLogger(GlobalPlatform.class); +public class GPSession { + private static final Logger logger = LoggerFactory.getLogger(GPSession.class); private static final String LFDBH_SHA1 = "SHA1"; public static final int SCP_ANY = 0; @@ -93,27 +93,23 @@ public class GlobalPlatform extends CardChannel implements AutoCloseable { protected boolean strict = true; GPSpec spec = GPSpec.GP211; - // (I)SD AID successfully selected or null - private AID sdAID = null; - // Either 1 or 2 or 3 - private int scpMajorVersion = 0; + // (I)SD AID + private AID sdAID; + GPSecureChannel scpVersion; private int scpKeyVersion = 0; private int blockSize = 255; - private GPSessionKeyProvider sessionKeys = null; + private GPSessionKeys sessionKeys; private SecureChannelWrapper wrapper = null; - private CardChannel channel; + private APDUBIBO channel; private GPRegistry registry = null; - private DMTokenGenerator tokenGenerator; + private DMTokenGenerator tokenizer = new DMTokenGenerator(null); private boolean dirty = true; // True if registry is dirty. - /** + /* * Maintaining locks to the underlying hardware is the duty of the caller - * - * @param channel channel to talk to - * @throws IllegalArgumentException if {@code channel} is null. */ - public GlobalPlatform(CardChannel channel, AID sdAID) { + public GPSession(APDUBIBO channel, AID sdAID) { if (channel == null) { throw new IllegalArgumentException("A card session is required"); } @@ -122,7 +118,7 @@ public GlobalPlatform(CardChannel channel, AID sdAID) { } // Try to find GlobalPlatform from a card - public static GlobalPlatform discover(CardChannel channel) throws GPException, CardException { + public static GPSession discover(APDUBIBO channel) throws GPException, IOException { if (channel == null) throw new IllegalArgumentException("channel is null"); @@ -174,14 +170,14 @@ public static GlobalPlatform discover(CardChannel channel) throws GPException, C if (isdaid != null && isdaid.getBytesValue().length > 0) { AID detectedAID = new AID(isdaid.getBytesValue()); logger.debug("Auto-detected ISD: " + detectedAID); - return new GlobalPlatform(channel, detectedAID); + return new GPSession(channel, detectedAID); } } throw new GPDataException("Could not auto-detect ISD AID", response.getData()); } // Establishes connection to a specific AID (selects it) - public static GlobalPlatform connect(CardChannel channel, AID sdAID) throws CardException, GPException { + public static GPSession connect(APDUBIBO channel, AID sdAID) throws IOException, GPException { if (channel == null) { throw new IllegalArgumentException("A card session is required"); } @@ -190,16 +186,16 @@ public static GlobalPlatform connect(CardChannel channel, AID sdAID) throws Card } logger.debug("(I)SD AID: " + sdAID); - GlobalPlatform gp = new GlobalPlatform(channel, sdAID); + GPSession gp = new GPSession(channel, sdAID); gp.select(sdAID); return gp; } - /** + /* * Get the version and build information of the library. */ public static String getVersion() { - try (InputStream versionfile = GlobalPlatform.class.getResourceAsStream("pro_version.txt")) { + try (InputStream versionfile = GPSession.class.getResourceAsStream("pro_version.txt")) { String version = "unknown-development"; if (versionfile != null) { try (BufferedReader vinfo = new BufferedReader(new InputStreamReader(versionfile, StandardCharsets.US_ASCII))) { @@ -218,10 +214,6 @@ public static byte[] getLoadParams(boolean loadParam, byte[] code) { : new byte[0]; } - @Override - public void close() { - // TODO explicitly closes SecureChannel, if connected. - } public void setStrict(boolean strict) { this.strict = strict; @@ -236,14 +228,14 @@ public void setSpec(GPSpec spec) { } public void setDMTokenGenerator(DMTokenGenerator tokenGenerator) { - this.tokenGenerator = tokenGenerator; + this.tokenizer = tokenGenerator; } public AID getAID() { return new AID(sdAID.getBytes()); } - public CardChannel getCardChannel() { + public APDUBIBO getCardChannel() { return channel; } @@ -260,7 +252,7 @@ public int getScpKeyVersion() { return scpKeyVersion; } - void select(AID sdAID) throws GPException, CardException { + void select(AID sdAID) throws GPException, IOException { // Try to select ISD (default selected) final CommandAPDU command = new CommandAPDU(ISO7816.CLA_ISO7816, ISO7816.INS_SELECT, 0x04, 0x00, sdAID.getBytes(), 256); ResponseAPDU resp = channel.transmit(command); @@ -351,23 +343,31 @@ private void setBlockSize(byte[] blocksize) { } } - List getKeyInfoTemplate() throws CardException, GPException { - List result = new ArrayList<>(); - result.addAll(GPData.get_key_template_list(GPData.fetchKeyInfoTemplate(this))); + List getKeyInfoTemplate() throws IOException, GPException { + List result = new ArrayList<>(); + final byte[] tmpl; + if (wrapper != null) { + // FIXME: check for 0x9000 + tmpl = transmit(new CommandAPDU(CLA_GP, ISO7816.INS_GET_DATA, 0x00, 0xE0, 256)).getData(); + } else { + tmpl = GPData.fetchKeyInfoTemplate(channel); + } + result.addAll(GPKeyInfo.parseTemplate(tmpl)); return result; } - /** + /* * Establishes a secure channel to the security domain. */ - public void openSecureChannel(GPSessionKeyProvider keys, byte[] host_challenge, int scpVersion, EnumSet securityLevel) - throws CardException, GPException { + public void openSecureChannel(GPCardKeys keys, byte[] host_challenge, int scpVersion, EnumSet securityLevel) + throws IOException, GPException { // ENC requires MAC if (securityLevel.contains(APDUMode.ENC)) { securityLevel.add(APDUMode.MAC); } + logger.info("Using card master keys: {}", keys); // DWIM: Generate host challenge if (host_challenge == null) { host_challenge = new byte[8]; @@ -404,77 +404,75 @@ public void openSecureChannel(GPSessionKeyProvider keys, byte[] host_challenge, scpKeyVersion = update_response[offset] & 0xFF; offset++; // Get major SCP version from Key Information field in response - scpMajorVersion = update_response[offset]; + this.scpVersion = GPSecureChannel.valueOf(update_response[offset] & 0xFF).orElseThrow(() -> new GPDataException("Invalid SCP version", update_response)); offset++; // get the protocol "i" parameter, if SCP03 int scp_i = -1; - if (scpMajorVersion == 3) { + if (this.scpVersion == GPSecureChannel.SCP03) { scp_i = update_response[offset]; offset++; } - // FIXME: SCP02 has 2 byte sequence + 6 bytes card challenge but the challenge is discarded ? // get card challenge byte card_challenge[] = Arrays.copyOfRange(update_response, offset, offset + 8); offset += card_challenge.length; + // get card cryptogram byte card_cryptogram[] = Arrays.copyOfRange(update_response, offset, offset + 8); offset += card_cryptogram.length; + // FIXME: detect if got to end logger.debug("Host challenge: " + HexUtils.bin2hex(host_challenge)); logger.debug("Card challenge: " + HexUtils.bin2hex(card_challenge)); - logger.debug("Card reports SCP0{}{} with key version {}", scpMajorVersion, (scpMajorVersion == 3 ? " i=" + String.format("%02x", scp_i) : ""), String.format("%d (0x%02X)", scpKeyVersion, scpKeyVersion)); + logger.debug("Card reports {}{} with key version {}", this.scpVersion, (this.scpVersion == GPSecureChannel.SCP03 ? " i=" + String.format("%02x", scp_i) : ""), String.format("%d (0x%02X)", scpKeyVersion, scpKeyVersion)); // Verify response // If using explicit key version, it must match. if ((keys.getVersion() > 0) && (scpKeyVersion != keys.getVersion())) { throw new GPException("Key version mismatch: " + keys.getVersion() + " != " + scpKeyVersion); } - - // FIXME: the whole SCP vs variants thing is broken in API and implementation - // Set default SCP version based on major version, if not explicitly known. - if (scpVersion != scpMajorVersion && scpVersion != SCP_ANY) { - logger.debug("Overriding SCP version: card reports " + scpMajorVersion + " but user requested " + scpVersion); - scpMajorVersion = scpVersion; - } - - // Set version for SC wrappers - if (scpMajorVersion == 1) { - scpVersion = SCP_01_05; - } else if (scpMajorVersion == 2) { - scpVersion = SCP_02_15; - } else if (scpMajorVersion == 3) { - scpVersion = 3; // FIXME: the symbolic numbering of versions needs to be fixed. - } - logger.debug("Will do SCP0{} ({})", scpMajorVersion, scpVersion); + logger.debug("Will do SCP0{}", this.scpVersion); // Remove RMAC if SCP01 TODO: this should be generic sanitizer somewhere - if (scpMajorVersion == 1 && securityLevel.contains(APDUMode.RMAC)) { + if (this.scpVersion == GPSecureChannel.SCP01 && securityLevel.contains(APDUMode.RMAC)) { logger.debug("SCP01 does not support RMAC, removing."); securityLevel.remove(APDUMode.RMAC); } // Extract ssc byte[] seq = null; - if (scpMajorVersion == 2) { + if (this.scpVersion == GPSecureChannel.SCP02) { seq = Arrays.copyOfRange(update_response, 12, 14); - } else if (scpMajorVersion == 3) { + } else if (this.scpVersion == GPSecureChannel.SCP03) { if (update_response.length == 32) { seq = Arrays.copyOfRange(update_response, 29, 32); } } - // Calculate session keys - keys.calculate(scpMajorVersion, diversification_data, host_challenge, card_challenge, seq); + // Give the card key a chance to be automatically diverisifed based on KDD + GPCardKeys cardKeys = keys.diversify(this.scpVersion, diversification_data); + + logger.info("Diversified card keys: {}", cardKeys); + + // Derive session keys + byte[] kdd; + if (this.scpVersion == GPSecureChannel.SCP02) { + kdd = seq.clone(); + } else { + kdd = GPUtils.concatenate(host_challenge, card_challenge); + } + + sessionKeys = cardKeys.getSessionKeys(kdd); + logger.info("Session keys: {}", sessionKeys); // Verify card cryptogram byte[] my_card_cryptogram = null; byte[] cntx = GPUtils.concatenate(host_challenge, card_challenge); - if (scpMajorVersion == 1 || scpMajorVersion == 2) { - my_card_cryptogram = GPCrypto.mac_3des_nulliv(keys.getKeyFor(GPSessionKeyProvider.KeyPurpose.ENC), cntx); + if (this.scpVersion == GPSecureChannel.SCP01 || this.scpVersion == GPSecureChannel.SCP02) { + my_card_cryptogram = GPCrypto.mac_3des_nulliv(sessionKeys.get(GPCardKeys.KeyPurpose.ENC), cntx); } else { - my_card_cryptogram = GPCrypto.scp03_kdf(keys.getKeyFor(GPSessionKeyProvider.KeyPurpose.MAC), (byte) 0x00, cntx, 64); + my_card_cryptogram = GPCrypto.scp03_kdf(sessionKeys.get(GPCardKeys.KeyPurpose.MAC), (byte) 0x00, cntx, 64); } // This is the main check for possible successful authentication. @@ -493,12 +491,12 @@ public void openSecureChannel(GPSessionKeyProvider keys, byte[] host_challenge, // Calculate host cryptogram and initialize SCP wrapper byte[] host_cryptogram = null; - if (scpMajorVersion == 1 || scpMajorVersion == 2) { - host_cryptogram = GPCrypto.mac_3des_nulliv(keys.getKeyFor(GPSessionKeyProvider.KeyPurpose.ENC), GPUtils.concatenate(card_challenge, host_challenge)); - wrapper = new SCP0102Wrapper(keys, scpVersion, EnumSet.of(APDUMode.MAC), null, null, blockSize); + if (this.scpVersion == GPSecureChannel.SCP01 || this.scpVersion == GPSecureChannel.SCP02) { + host_cryptogram = GPCrypto.mac_3des_nulliv(sessionKeys.get(GPCardKeys.KeyPurpose.ENC), GPUtils.concatenate(card_challenge, host_challenge)); + wrapper = new SCP0102Wrapper(sessionKeys, scpVersion, EnumSet.of(APDUMode.MAC), null, null, blockSize); } else { - host_cryptogram = GPCrypto.scp03_kdf(keys.getKeyFor(GPSessionKeyProvider.KeyPurpose.MAC), (byte) 0x01, cntx, 64); - wrapper = new SCP03Wrapper(keys, scpVersion, EnumSet.of(APDUMode.MAC), null, null, blockSize); + host_cryptogram = GPCrypto.scp03_kdf(sessionKeys.get(GPCardKeys.KeyPurpose.MAC), (byte) 0x01, cntx, 64); + wrapper = new SCP03Wrapper(sessionKeys, scpVersion, EnumSet.of(APDUMode.MAC), null, null, blockSize); } logger.debug("Calculated host cryptogram: " + HexUtils.bin2hex(host_cryptogram)); @@ -507,12 +505,10 @@ public void openSecureChannel(GPSessionKeyProvider keys, byte[] host_challenge, response = transmit(externalAuthenticate); GPException.check(response, "External authenticate failed"); - // Store reference for commands - sessionKeys = keys; wrapper.setSecurityLevel(securityLevel); // FIXME: ugly stuff, ugly... - if (scpMajorVersion != 3) { + if (this.scpVersion != GPSecureChannel.SCP03) { SCP0102Wrapper w = (SCP0102Wrapper) wrapper; if (securityLevel.contains(APDUMode.RMAC)) { w.setRMACIV(w.getIV()); @@ -520,84 +516,86 @@ public void openSecureChannel(GPSessionKeyProvider keys, byte[] host_challenge, } } - // Exist to be able to pass around a transmit method - @Override - public Card getCard() { - return null; - } - - @Override - public int getChannelNumber() { - return 0; - } - - @Override - public ResponseAPDU transmit(CommandAPDU command) throws CardException { + // Pipe through secure channel + public ResponseAPDU transmit(CommandAPDU command) throws IOException { try { - CommandAPDU wc = wrapper.wrap(command); - ResponseAPDU wr = channel.transmit(wc); - return wrapper.unwrap(wr); + // TODO: BIBO pretty printer + //logger.trace("PT> {}", HexUtils.bin2hex(command.getBytes())); + ResponseAPDU unwrapped = wrapper.unwrap(channel.transmit(wrapper.wrap(command))); + //logger.trace("PT < {}", HexUtils.bin2hex(unwrapped.getBytes())); + return unwrapped; } catch (GPException e) { - throw new CardException("Secure channel failure: " + e.getMessage(), e); + throw new IOException("Secure channel failure: " + e.getMessage(), e); } } - private ResponseAPDU transmitLV(CommandAPDU command) throws CardException { - logger.trace("Payload: "); - // TODO - Next line causes exception at java.util.Arrays.copyOfRange in trace_lv if data has token appended - //GPUtils.trace_lv(command.getData(), logger); + // given a LV APDU content, pretty-print into log + private ResponseAPDU transmitLV(CommandAPDU command) throws IOException { + logger.trace("LV payload: "); + try { + GPUtils.trace_lv(command.getData(), logger); + } catch (Exception e) { + logger.error("Invalid LV: {}" + HexUtils.bin2hex(command.getData())); + } return transmit(command); } - private ResponseAPDU transmitDM(CommandAPDU command) throws CardException { - command = tokenGenerator.applyToken(command); - return transmitLV(command); - } - - @Override - public int transmit(ByteBuffer byteBuffer, ByteBuffer byteBuffer1) throws CardException { - throw new IllegalStateException("Use the other transmit"); + private ResponseAPDU transmitTLV(CommandAPDU command) throws IOException { + logger.trace("TLV payload: "); + try { + GPUtils.trace_tlv(command.getData(), logger); + } catch (Exception e) { + logger.error("Invalid TLV: {}" + HexUtils.bin2hex(command.getData())); + } + return transmit(command); } - public int getSCPVersion() { - return scpMajorVersion; + private CommandAPDU tokenize(CommandAPDU command) throws IOException { + try { + command = tokenizer.applyToken(command); + return command; + } catch (GeneralSecurityException e) { + logger.error("Can not apply token: " + e.getMessage(), e); + throw new GPException("Can not apply DM token", e); + } } - public void loadCapFile(CAPFile cap, AID targetDomain) throws CardException, GPException { + // TODO: clean up this mess + public void loadCapFile(CAPFile cap, AID targetDomain) throws IOException, GPException { if (targetDomain == null) targetDomain = sdAID; loadCapFile(cap, targetDomain, false, false, null, null, LFDBH_SHA1); } - public void loadCapFile(CAPFile cap, AID targetDomain, String hashFunction) throws CardException, GPException { + public void loadCapFile(CAPFile cap, AID targetDomain, String hashFunction) throws IOException, GPException { if (targetDomain == null) targetDomain = sdAID; loadCapFile(cap, targetDomain, false, false, null, null, hashFunction); } - public void loadCapFile(CAPFile cap, AID targetDomain, byte[] dap, String hash) throws CardException, GPException { + public void loadCapFile(CAPFile cap, AID targetDomain, byte[] dap, String hash) throws IOException, GPException { if (targetDomain == null) targetDomain = sdAID; loadCapFile(cap, targetDomain, false, false, targetDomain, dap, hash); } - public void loadCapFile(CAPFile cap, AID targetDomain, AID dapdomain, byte[] dap, String hashFunction) throws CardException, GPException { + public void loadCapFile(CAPFile cap, AID targetDomain, AID dapdomain, byte[] dap, String hashFunction) throws IOException, GPException { if (targetDomain == null) targetDomain = sdAID; loadCapFile(cap, targetDomain, false, false, dapdomain, dap, hashFunction); } private void loadCapFile(CAPFile cap, AID targetDomain, boolean includeDebug, boolean loadParam, AID dapDomain, byte[] dap, String hashFunction) - throws GPException, CardException { + throws GPException, IOException { if (getRegistry().allAIDs().contains(cap.getPackageAID())) { giveStrictWarning("Package with AID " + cap.getPackageAID() + " is already present on card"); } // FIXME: hash type handling needs to be sensible. - boolean isHashRequired = dap != null || tokenGenerator.hasKey(); - byte[] hash = isHashRequired ? cap.getLoadFileDataHash(hashFunction, includeDebug) : new byte[0]; - byte[] code = cap.getCode(includeDebug); + boolean isHashRequired = dap != null || tokenizer.hasKey(); + byte[] hash = isHashRequired ? cap.getLoadFileDataHash(hashFunction) : new byte[0]; + byte[] code = cap.getCode(); // FIXME: parameters are optional for load byte[] loadParams = getLoadParams(loadParam, code); @@ -620,7 +618,8 @@ private void loadCapFile(CAPFile cap, AID targetDomain, boolean includeDebug, bo } CommandAPDU command = new CommandAPDU(CLA_GP, INS_INSTALL, P1_INSTALL_FOR_LOAD, 0x00, bo.toByteArray()); - ResponseAPDU response = transmitDM(command); + command = tokenize(command); + ResponseAPDU response = transmitLV(command); GPException.check(response, "INSTALL [for load] failed"); // Construct load block @@ -658,28 +657,7 @@ private void loadCapFile(CAPFile cap, AID targetDomain, boolean includeDebug, bo dirty = true; } - /** - * Install an applet and make it selectable. The package and applet AID must - * be present (ie. non-null). If one of the other parameters is null - * sensible defaults are chosen. If installation parameters are used, they - * must be passed in a special format, see parameter description below. - *

- * Before installation the package containing the applet must be loaded onto - * the card, see {@link #loadCapFile loadCapFile}. - *

- * This method installs just one applet. Call it several times for packages - * containing several applets. - * - * @param packageAID the package that containing the applet - * @param appletAID the applet to be installed - * @param instanceAID the applet AID passed to the install method of the applet, - * defaults to {@code packageAID} if null - * @param privileges privileges encoded as an object - * @param installParams tagged installation parameters, defaults to {@code 0xC9 00} - * (ie. no installation parameters) if null, if non-null the - * format is {@code 0xC9 len data...} - */ - public void installAndMakeSelectable(AID packageAID, AID appletAID, AID instanceAID, Privileges privileges, byte[] installParams) throws GPException, CardException { + public void installAndMakeSelectable(AID packageAID, AID appletAID, AID instanceAID, Privileges privileges, byte[] installParams) throws GPException, IOException { if (instanceAID == null) { instanceAID = appletAID; } @@ -689,33 +667,13 @@ public void installAndMakeSelectable(AID packageAID, AID appletAID, AID instance byte[] data = buildInstallData(packageAID, appletAID, instanceAID, privileges, installParams); CommandAPDU command = new CommandAPDU(CLA_GP, INS_INSTALL, P1_INSTALL_AND_MAKE_SELECTABLE, 0x00, data); - ResponseAPDU response = transmitDM(command); + command = tokenize(command); + ResponseAPDU response = transmitLV(command); GPException.check(response, "INSTALL [for install and make selectable] failed"); dirty = true; } - /** - * Install an applet. Do not make it selectable. The package and applet AID must - * be present (ie. non-null). If one of the other parameters is null - * sensible defaults are chosen. If installation parameters are used, they - * must be passed in a special format, see parameter description below. - *

- * Before installation the package containing the applet must be loaded onto - * the card, see {@link #loadCapFile loadCapFile}. - *

- * This method installs just one applet. Call it several times for packages - * containing several applets. - * - * @param packageAID the package that containing the applet - * @param appletAID the applet to be installed - * @param instanceAID the applet AID passed to the install method of the applet, - * defaults to {@code packageAID} if null - * @param privileges privileges encoded as an object - * @param installParams tagged installation parameters, defaults to {@code 0xC9 00} - * (ie. no installation parameters) if null, if non-null the - * format is {@code 0xC9 len data...} - */ - public void installForInstall(AID packageAID, AID appletAID, AID instanceAID, Privileges privileges, byte[] installParams, PrivateKey key) throws GPException, CardException { + public void installForInstall(AID packageAID, AID appletAID, AID instanceAID, Privileges privileges, byte[] installParams, PrivateKey key) throws GPException, IOException { if (instanceAID == null) { instanceAID = appletAID; } @@ -725,7 +683,8 @@ public void installForInstall(AID packageAID, AID appletAID, AID instanceAID, Pr byte[] data = buildInstallData(packageAID, appletAID, instanceAID, privileges, installParams); CommandAPDU command = new CommandAPDU(CLA_GP, INS_INSTALL, P1_INSTALL_FOR_INSTALL, 0x00, data); - ResponseAPDU response = transmitDM(command); + command = tokenize(command); + ResponseAPDU response = transmitLV(command); GPException.check(response, "INSTALL [for install] failed"); dirty = true; } @@ -768,7 +727,7 @@ private byte[] buildInstallData(AID packageAID, AID appletAID, AID instanceAID, return bo.toByteArray(); } - public void extradite(AID what, AID to) throws GPException, CardException { + public void extradite(AID what, AID to) throws GPException, IOException { // GP 2.2.1 Table 11-45 ByteArrayOutputStream bo = new ByteArrayOutputStream(); try { @@ -787,13 +746,14 @@ public void extradite(AID what, AID to) throws GPException, CardException { } CommandAPDU command = new CommandAPDU(CLA_GP, INS_INSTALL, 0x10, 0x00, bo.toByteArray()); - ResponseAPDU response = transmitDM(command); + command = tokenize(command); + ResponseAPDU response = transmitLV(command); GPException.check(response, "INSTALL [for extradition] failed"); dirty = true; } - public void installForPersonalization(AID aid) throws CardException, GPException { + public void installForPersonalization(AID aid) throws IOException, GPException { // send the INSTALL for personalization command ByteArrayOutputStream bo = new ByteArrayOutputStream(); try { @@ -813,31 +773,27 @@ public void installForPersonalization(AID aid) throws CardException, GPException } - public byte[] personalizeSingle(AID aid, byte[] data, int P1) throws CardException, GPException { + public byte[] personalizeSingle(AID aid, byte[] data, int P1) throws IOException, GPException { return personalize(aid, Collections.singletonList(data), P1).get(0); } - /** + /* * Sends STORE DATA commands to the application identified via SD - * - * @param aid - AID of the target application (or Security Domain) - * @throws GPException - * @throws CardException */ - public void personalize(AID aid, byte[] data, int P1) throws CardException, GPException { + public void personalize(AID aid, byte[] data, int P1) throws IOException, GPException { installForPersonalization(aid); // Now pump the data storeData(data, P1); } - public List personalize(AID aid, List data, int P1) throws CardException, GPException { + public List personalize(AID aid, List data, int P1) throws IOException, GPException { installForPersonalization(aid); return storeData(data, P1); } - public byte[] storeDataSingle(byte[] data, int P1) throws CardException, GPException { + public byte[] storeDataSingle(byte[] data, int P1) throws IOException, GPException { if (data.length > wrapper.getBlockSize()) { throw new IllegalArgumentException("block size is bigger than possibility to send: " + data.length + ">" + wrapper.getBlockSize()); } @@ -845,13 +801,13 @@ public byte[] storeDataSingle(byte[] data, int P1) throws CardException, GPExcep } // Send a GP-formatted STORE DATA block, splitting it into pieces if/as necessary - public void storeData(byte[] data, int P1) throws CardException, GPException { + public void storeData(byte[] data, int P1) throws IOException, GPException { List blocks = GPUtils.splitArray(data, wrapper.getBlockSize()); storeData(blocks, P1); } // Send a GP-formatted STORE DATA blocks - public List storeData(List blocks, int P1) throws CardException, GPException { + public List storeData(List blocks, int P1) throws IOException, GPException { List result = new ArrayList<>(); for (int i = 0; i < blocks.size(); i++) { int p1 = (i == (blocks.size() - 1)) ? P1 | 0x80 : P1 & 0x7F; @@ -861,12 +817,12 @@ public List storeData(List blocks, int P1) throws CardException, return result; } - byte[] _storeDataSingle(byte[] data, int P1, int P2) throws CardException, GPException { + byte[] _storeDataSingle(byte[] data, int P1, int P2) throws IOException, GPException { CommandAPDU store = new CommandAPDU(CLA_GP, INS_STORE_DATA, P1, P2, data, 256); return GPException.check(transmit(store), "STORE DATA failed").getData(); } - public void makeDefaultSelected(AID aid) throws CardException, GPException { + public void makeDefaultSelected(AID aid) throws IOException, GPException { // FIXME: only works for some 2.1.1 cards ? Clarify and document ByteArrayOutputStream bo = new ByteArrayOutputStream(); // Only supported privilege. @@ -886,19 +842,20 @@ public void makeDefaultSelected(AID aid) throws CardException, GPException { } CommandAPDU command = new CommandAPDU(CLA_GP, INS_INSTALL, 0x08, 0x00, bo.toByteArray()); - ResponseAPDU response = transmitDM(command); + command = tokenize(command); + ResponseAPDU response = transmitLV(command); GPException.check(response, "INSTALL [for make selectable] failed"); dirty = true; } - public void lockUnlockApplet(AID app, boolean lock) throws CardException, GPException { + public void lockUnlockApplet(AID app, boolean lock) throws IOException, GPException { CommandAPDU cmd = new CommandAPDU(CLA_GP, INS_SET_STATUS, 0x40, lock ? 0x80 : 0x00, app.getBytes()); ResponseAPDU response = transmit(cmd); GPException.check(response, "SET STATUS failed"); dirty = true; } - public void setCardStatus(byte status) throws CardException, GPException { + public void setCardStatus(byte status) throws IOException, GPException { logger.debug("Setting status to {}", GPRegistryEntry.getLifeCycleString(Kind.IssuerSecurityDomain, status)); CommandAPDU cmd = new CommandAPDU(CLA_GP, INS_SET_STATUS, 0x80, status); ResponseAPDU response = transmit(cmd); @@ -906,15 +863,11 @@ public void setCardStatus(byte status) throws CardException, GPException { dirty = true; } - /** + /* * Delete file {@code aid} on the card. Delete dependencies as well if * {@code deleteDeps} is true. - * - * @param aid identifier of the file to delete - * @param deleteDeps if true delete dependencies as well - * @throws CardException for low-level communication errors */ - public void deleteAID(AID aid, boolean deleteDeps) throws GPException, CardException { + public void deleteAID(AID aid, boolean deleteDeps) throws GPException, IOException { ByteArrayOutputStream bo = new ByteArrayOutputStream(); try { bo.write(0x4f); @@ -924,12 +877,13 @@ public void deleteAID(AID aid, boolean deleteDeps) throws GPException, CardExcep throw new RuntimeException(ioe); } CommandAPDU command = new CommandAPDU(CLA_GP, INS_DELETE, 0x00, deleteDeps ? 0x80 : 0x00, bo.toByteArray()); - ResponseAPDU response = transmitDM(command); - GPException.check(response, "Deletion failed"); + command = tokenize(command); + ResponseAPDU response = transmitTLV(command); + GPException.check(response, "DELETE failed"); dirty = true; } - public void deleteKey(int keyver) throws GPException, CardException { + public void deleteKey(int keyver) throws GPException, IOException { // FIXME: no card seems to support it ByteArrayOutputStream bo = new ByteArrayOutputStream(); //bo.write(0xd0); @@ -951,71 +905,48 @@ public void deleteKey(int keyver) throws GPException, CardException { GPException.check(response, "Deletion failed"); } - public void renameISD(AID newaid) throws GPException, CardException { + public void renameISD(AID newaid) throws GPException, IOException { CommandAPDU rename = new CommandAPDU(CLA_GP, INS_STORE_DATA, 0x90, 0x00, GPUtils.concatenate(new byte[]{0x4f, (byte) newaid.getLength()}, newaid.getBytes())); ResponseAPDU response = transmit(rename); GPException.check(response, "Rename failed"); } - // FIXME: remove the withCheck parameter, as always true? - private byte[] encodeKey(GPKey key, GPKey dek, boolean withCheck) { + private byte[] encodeKey(GPSessionKeys dek, GPCardKeys other, KeyPurpose p) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - if (key.getType() == Type.DES3) { - // Encrypt key with DEK - Cipher cipher; - cipher = Cipher.getInstance(GPCrypto.DES3_ECB_CIPHER); - cipher.init(Cipher.ENCRYPT_MODE, dek.getKeyAs(Type.DES3)); - byte[] cgram = cipher.doFinal(key.getBytes(), 0, 16); - baos.write(0x80); // 3DES - baos.write(cgram.length); // Length - baos.write(cgram); - if (withCheck) { - byte[] kcv = GPCrypto.kcv_3des(key); - baos.write(kcv.length); - baos.write(kcv); - } else { - baos.write(0); - } - } else if (key.getType() == Type.AES) { + if (other.scp == GPSecureChannel.SCP03) { // baos.write(0xFF); - byte[] cgram = GPCrypto.scp03_encrypt_key(dek, key); - byte[] check = GPCrypto.scp03_key_check_value(key); + byte[] cgram = dek.encryptKey(other, p); + byte[] check = other.kcv(p); baos.write(0x88); // AES baos.write(cgram.length + 1); - baos.write(key.getLength()); + //baos.write(key.getLength()); // FIXME: KeyInfo + baos.write(16); // FIXME: KeyInfo baos.write(cgram); baos.write(check.length); baos.write(check); - } else { - throw new IllegalArgumentException("Don't know how to handle " + key.getType()); + } else if (other.scp == GPSecureChannel.SCP01 || other.scp == GPSecureChannel.SCP02) { + // Encrypt key with DEK + byte[] cgram = dek.encryptKey(other, p); + baos.write(0x80); // 3DES + baos.write(cgram.length); // Length + baos.write(cgram); + byte[] kcv = other.kcv(p); + baos.write(kcv.length); + baos.write(kcv); } return baos.toByteArray(); } catch (IOException | GeneralSecurityException e) { - throw new RuntimeException(e); + throw new GPException("Could not wrap key", e); } } - public void putKeys(List keys, boolean replace) throws GPException, CardException { - // Check for sanity and usability - if (keys.size() < 1 || keys.size() > 3) { - throw new IllegalArgumentException("Can add 1 or up to 3 keys at a time"); - } - if (keys.size() > 1) { - for (int i = 1; i < keys.size(); i++) { - if (keys.get(i - 1).getID() != keys.get(i).getID() - 1) { - throw new IllegalArgumentException("Key ID-s of multiple keys must be sequential!"); - } - } - } + public void putKeys(GPCardKeys keys, boolean replace) throws GPException, IOException { // Log and trace - logger.debug("PUT KEY version {}", keys.get(0).getVersion()); - for (GPKey k : keys) { - logger.trace("PUT KEY:" + k); - } + logger.debug("PUT KEY version {}", keys); // Check consistency, if template is available. - List tmpl = getKeyInfoTemplate(); + List tmpl = getKeyInfoTemplate(); if (tmpl.size() > 0) { // // TODO: move to GPTool @@ -1047,21 +978,21 @@ public void putKeys(List keys, boolean replace) throws GPException, CardE // Construct APDU int P1 = 0x00; // New key in single command unless replace if (replace) { - P1 = keys.get(0).getVersion(); - } - int P2 = keys.get(0).getID(); - if (keys.size() > 1) { - P2 |= 0x80; + P1 = keys.getVersion(); } + // int P2 = keys.get(0).getID(); + int P2 = 0x01; + P2 |= 0x80; // More than one key + ByteArrayOutputStream bo = new ByteArrayOutputStream(); try { // New key version - bo.write(keys.get(0).getVersion()); + bo.write(keys.getVersion()); // Key data - for (GPKey k : keys) { - bo.write(encodeKey(k, sessionKeys.getKeyFor(GPSessionKeyProvider.KeyPurpose.DEK), true)); + for (KeyPurpose p : KeyPurpose.cardKeys()) { + bo.write(encodeKey(sessionKeys, keys, p)); } - } catch (IOException e) { + } catch (Exception e) { throw new RuntimeException(e); } @@ -1070,8 +1001,9 @@ public void putKeys(List keys, boolean replace) throws GPException, CardE GPException.check(response, "PUT KEY failed"); } + // Puts a RSA public key for DAP purposes (format 1) - public void putKey(RSAPublicKey pubkey, int version) throws CardException, GPException { + public void putKey(RSAPublicKey pubkey, int version) throws IOException, GPException { ByteArrayOutputStream bo = new ByteArrayOutputStream(); try { @@ -1094,7 +1026,7 @@ public void putKey(RSAPublicKey pubkey, int version) throws CardException, GPExc GPException.check(response, "PUT KEY failed"); } - public GPRegistry getRegistry() throws GPException, CardException { + public GPRegistry getRegistry() throws GPException, IOException { if (dirty) { registry = getStatus(); dirty = false; @@ -1103,7 +1035,7 @@ public GPRegistry getRegistry() throws GPException, CardException { } // TODO: The way registry parsing mode is piggybacked to the registry class is not really nice. - private byte[] getConcatenatedStatus(GPRegistry reg, int p1, byte[] data) throws CardException, GPException { + private byte[] getConcatenatedStatus(GPRegistry reg, int p1, byte[] data) throws IOException, GPException { // By default use tags int p2 = reg.tags ? 0x02 : 0x00; @@ -1153,7 +1085,7 @@ private byte[] getConcatenatedStatus(GPRegistry reg, int p1, byte[] data) throws return bo.toByteArray(); } - private GPRegistry getStatus() throws CardException, GPException { + private GPRegistry getStatus() throws IOException, GPException { GPRegistry registry = new GPRegistry(); if (spec == GPSpec.OP201) { diff --git a/library/src/main/java/pro/javacard/gp/GPSessionKeys.java b/library/src/main/java/pro/javacard/gp/GPSessionKeys.java new file mode 100644 index 00000000..f60859ac --- /dev/null +++ b/library/src/main/java/pro/javacard/gp/GPSessionKeys.java @@ -0,0 +1,49 @@ +package pro.javacard.gp; + +import apdu4j.HexUtils; +import pro.javacard.gp.GPCardKeys.KeyPurpose; + +import java.security.GeneralSecurityException; + +public class GPSessionKeys { + GPCardKeys cardKeys; + + private final byte[] enc; + private final byte[] mac; + private final byte[] rmac; + + + public GPSessionKeys(GPCardKeys cardKeys, byte[] enc, byte[] mac, byte[] rmac) { + this.cardKeys = cardKeys; + this.enc = enc.clone(); + this.mac = mac.clone(); + this.rmac = rmac.clone(); + } + + // Encrypts padded data, either with session DEK (SCP02) or card DEK (SCP01 and SCP03) + public byte[] encrypt(byte[] data) throws GeneralSecurityException { + return cardKeys.encrypt(data); + } + + public byte[] encryptKey(GPCardKeys other, KeyPurpose p) throws GeneralSecurityException { + return cardKeys.encryptKey(other, p); + } + + public byte[] get(KeyPurpose p) { + switch (p) { + case ENC: + return enc.clone(); + case MAC: + return mac.clone(); + case RMAC: + return rmac.clone(); + default: + throw new IllegalArgumentException("Invalid session key: " + p); + } + } + + @Override + public String toString() { + return String.format("ENC=%s MAC=%s RMAC=%s, card keys=%s", HexUtils.bin2hex(enc), HexUtils.bin2hex(mac), rmac == null ? "N/A" : HexUtils.bin2hex(rmac), cardKeys.toString()); + } +} diff --git a/src/main/java/pro/javacard/gp/GPUtils.java b/library/src/main/java/pro/javacard/gp/GPUtils.java similarity index 98% rename from src/main/java/pro/javacard/gp/GPUtils.java rename to library/src/main/java/pro/javacard/gp/GPUtils.java index eaa055e2..8af346e9 100644 --- a/src/main/java/pro/javacard/gp/GPUtils.java +++ b/library/src/main/java/pro/javacard/gp/GPUtils.java @@ -116,12 +116,11 @@ public static byte[] encodeLength(int len) { // Encodes APDU LC value, which has either length of 1 byte or 3 bytes (for extended length APDUs) // If LC is bigger than fits in one byte (255), LC must be encoded in three bytes public static byte[] encodeLcLength(int lc) { - if(lc>255) { + if (lc > 255) { byte[] lc_ba = ByteBuffer.allocate(4).putInt(lc).array(); return Arrays.copyOfRange(lc_ba, 1, 4); - } - else - return new byte[]{(byte)lc}; + } else + return new byte[]{(byte) lc}; } // Assumes the bignum length must be even @@ -154,6 +153,7 @@ static void trace_tlv(byte[] data, Logger l) { public boolean isDebugEnabled() { return true; } + @Override public void debug(String s, Object... objects) { l.trace(s, objects); diff --git a/library/src/main/java/pro/javacard/gp/ISO7816.java b/library/src/main/java/pro/javacard/gp/ISO7816.java new file mode 100644 index 00000000..7c3f3960 --- /dev/null +++ b/library/src/main/java/pro/javacard/gp/ISO7816.java @@ -0,0 +1,71 @@ +package pro.javacard.gp; + +public class ISO7816 { + public static final int OFFSET_CLA = 0; + public static final int OFFSET_INS = 1; + public static final int OFFSET_P1 = 2; + public static final int OFFSET_P2 = 3; + public static final int OFFSET_LC = 4; + public static final int OFFSET_CDATA = 5; + public static final int CLA_ISO7816 = 0x00; + public static final int INS_ERASE_BINARY_0E = 0x0E; + public static final int INS_VERIFY_20 = 0x20; + public static final int INS_CHANGE_CHV_24 = 0x24; + public static final int INS_UNBLOCK_CHV_2C = 0x2C; + public static final int INS_EXTERNAL_AUTHENTICATE_82 = 0x82; + public static final int INS_MUTUAL_AUTHENTICATE_82 = 0x82; + public static final int INS_GET_CHALLENGE_84 = 0x84; + public static final int INS_ASK_RANDOM = 0x84; + public static final int INS_GIVE_RANDOM = 0x86; + public static final int INS_INTERNAL_AUTHENTICATE = 0x88; + public static final int INS_SEEK = 0xA2; + public static final int INS_SELECT = 0xA4; + public static final int INS_SELECT_FILE = 0xA4; + public static final int INS_CLOSE_APPLICATION = 0xAC; + public static final int INS_READ_BINARY = 0xB0; + public static final int INS_READ_BINARY2 = 0xB1; + public static final int INS_READ_RECORD = 0xB2; + public static final int INS_READ_RECORD2 = 0xB3; + public static final int INS_READ_RECORDS = 0xB2; + public static final int INS_GET_RESPONSE = 0xC0; + public static final int INS_ENVELOPE = 0xC2; + public static final int INS_GET_DATA = 0xCA; + public static final int INS_WRITE_BINARY = 0xD0; + public static final int INS_WRITE_RECORD = 0xD2; + public static final int INS_UPDATE_BINARY = 0xD6; + public static final int INS_LOAD_KEY_FILE = 0xD8; + public static final int INS_PUT_DATA = 0xDA; + public static final int INS_UPDATE_RECORD = 0xDC; + public static final int INS_CREATE_FILE = 0xE0; + public static final int INS_APPEND_RECORD = 0xE2; + public static final int INS_DELETE_FILE = 0xE4; + + public static final int SW_BYTES_REMAINING_00 = 0x6100; + public static final int SW_END_OF_FILE = 0x6282; + public static final int SW_LESS_DATA_RESPONDED_THAN_REQUESTED = 0x6287; + public static final int SW_WRONG_LENGTH = 0x6700; + public static final int SW_SECURITY_STATUS_NOT_SATISFIED = 0x6982; + public static final int SW_AUTHENTICATION_METHOD_BLOCKED = 0x6983; + public static final int SW_DATA_INVALID = 0x6984; + public static final int SW_CONDITIONS_OF_USE_NOT_SATISFIED = 0x6985; + public static final int SW_COMMAND_NOT_ALLOWED = 0x6986; + public static final int SW_EXPECTED_SM_DATA_OBJECTS_MISSING = 0x6987; + public static final int SW_SM_DATA_OBJECTS_INCORRECT = 0x6988; + public static final int SW_KEY_USAGE_ERROR = 0x69C1; + public static final int SW_WRONG_DATA = 0x6A80; + public static final int SW_FILEHEADER_INCONSISTENT = 0x6A80; + public static final int SW_FUNC_NOT_SUPPORTED = 0x6A81; + public static final int SW_FILE_NOT_FOUND = 0x6A82; + public static final int SW_RECORD_NOT_FOUND = 0x6A83; + public static final int SW_FILE_FULL = 0x6A84; + public static final int SW_OUT_OF_MEMORY = 0x6A84; + public static final int SW_INCORRECT_P1P2 = 0x6A86; + public static final int SW_KEY_NOT_FOUND = 0x6A88; + public static final int SW_WRONG_P1P2 = 0x6B00; + public static final int SW_CORRECT_LENGTH_00 = 0x6C00; + public static final int SW_INS_NOT_SUPPORTED = 0x6D00; + public static final int SW_CLA_NOT_SUPPORTED = 0x6E00; + public static final int SW_NO_PRECISE_DIAGNOSIS = 0x6F00; + public static final int SW_CARD_TERMINATED = 0x6FFF; + public static final int SW_NO_ERROR = 0x9000; +} diff --git a/library/src/main/java/pro/javacard/gp/PlaintextKeys.java b/library/src/main/java/pro/javacard/gp/PlaintextKeys.java new file mode 100644 index 00000000..df48e7ed --- /dev/null +++ b/library/src/main/java/pro/javacard/gp/PlaintextKeys.java @@ -0,0 +1,373 @@ +/* + * GlobalPlatformPro - GlobalPlatform tool + * + * Copyright (C) 2015-2019 Martin Paljak, martin@martinpaljak.net + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package pro.javacard.gp; + +import apdu4j.HexUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +// Handles plaintext card keys. +// Supports diversification of card keys with a few known algorithms. +public class PlaintextKeys extends GPCardKeys { + private static final Logger logger = LoggerFactory.getLogger(PlaintextKeys.class); + + // After diversify() we know for which protocol we have keys for, unless known before + static final byte[] defaultKeyBytes = HexUtils.hex2bin("404142434445464748494A4B4C4D4E4F"); + + // Derivation constants + public static final Map SCP02_CONSTANTS; + public static final Map SCP03_CONSTANTS; + public static final Map SCP03_KDF_CONSTANTS; + + static { + HashMap scp2 = new HashMap<>(); + scp2.put(KeyPurpose.MAC, new byte[]{(byte) 0x01, (byte) 0x01}); + scp2.put(KeyPurpose.RMAC, new byte[]{(byte) 0x01, (byte) 0x02}); + scp2.put(KeyPurpose.DEK, new byte[]{(byte) 0x01, (byte) 0x81}); + scp2.put(KeyPurpose.ENC, new byte[]{(byte) 0x01, (byte) 0x82}); + SCP02_CONSTANTS = Collections.unmodifiableMap(scp2); + + HashMap scp3 = new HashMap<>(); + scp3.put(KeyPurpose.ENC, (byte) 0x04); + scp3.put(KeyPurpose.MAC, (byte) 0x06); + scp3.put(KeyPurpose.RMAC, (byte) 0x07); + SCP03_CONSTANTS = Collections.unmodifiableMap(scp3); + + HashMap scp3kdf = new HashMap<>(); + scp3kdf.put(KeyPurpose.ENC, HexUtils.hex2bin("0000000100")); + scp3kdf.put(KeyPurpose.MAC, HexUtils.hex2bin("0000000200")); + scp3kdf.put(KeyPurpose.DEK, HexUtils.hex2bin("0000000300")); + SCP03_KDF_CONSTANTS = Collections.unmodifiableMap(scp3kdf); + } + + // If diverisification is to be used, which method + Diversification diversifier; + + // Keyset version + private int version = 0; + private int id = 0; + + // Holds card-specific keys. They shall be diversified in-place, as needed + private HashMap cardKeys = new HashMap<>(); + + // Holds a copy of session-specific keys + private HashMap sessionKeys = new HashMap<>(); + + private PlaintextKeys(byte[] enc, byte[] mac, byte[] dek, Diversification d) { + cardKeys.put(KeyPurpose.ENC, enc); + cardKeys.put(KeyPurpose.MAC, mac); + cardKeys.put(KeyPurpose.DEK, dek); + diversifier = d; + } + + public static PlaintextKeys fromMasterKey(byte[] master) { + return derivedFromMasterKey(master, null, Diversification.NONE); + } + + public static PlaintextKeys fromMasterKey(byte[] master, byte[] kcv) { + return derivedFromMasterKey(master, kcv, Diversification.NONE); + } + + public static PlaintextKeys defaultKey() { + return derivedFromMasterKey(defaultKeyBytes, null, Diversification.NONE); + } + + public static PlaintextKeys derivedFromMasterKey(byte[] master, byte[] kcv, Diversification div) { + if (kcv != null && kcv.length == 3) { + byte[] kcv_des = GPCrypto.kcv_3des(master); + byte[] kcv_aes = GPCrypto.kcv_aes(master); + if (Arrays.equals(kcv_des, kcv)) { + logger.debug("KCV matches 3DES"); + } else if (Arrays.equals(kcv_aes, kcv)) { + logger.debug("KCV matches AES"); + } else { + String msg = String.format("KCV mismatch: %s vs %s (3DES) or %s (AES)", HexUtils.bin2hex(kcv), HexUtils.bin2hex(kcv_des), HexUtils.bin2hex(kcv_aes)); + throw new IllegalArgumentException(msg); + } + } + return new PlaintextKeys(master, master, master, div); + } + + + public static PlaintextKeys fromKeys(byte[] enc, byte[] mac, byte[] dek) { + return new PlaintextKeys(enc, mac, dek, Diversification.NONE); + } + + // Purpose defines the magic constants for diversification + public static byte[] diversify(byte[] k, KeyPurpose usage, byte[] kdd, Diversification method) throws GPException { + try { + final byte[] kv; + if (method == Diversification.KDF3) { + return GPCrypto.scp03_kdf(k, new byte[]{}, GPUtils.concatenate(SCP03_KDF_CONSTANTS.get(usage), kdd), k.length); + } else { + // shift around and fill initialize update data as required. + if (method == Diversification.VISA2) { + kv = fillVisa2(kdd, usage); + } else if (method == Diversification.EMV) { + kv = fillEmv(kdd, usage); + } else + throw new IllegalStateException("Unknown diversification method"); + + Cipher cipher = Cipher.getInstance(GPCrypto.DES3_ECB_CIPHER); + cipher.init(Cipher.ENCRYPT_MODE, GPCrypto.des3key(k)); + return cipher.doFinal(kv); + } + } catch (BadPaddingException | InvalidKeyException | IllegalBlockSizeException e) { + throw new GPException("Diversification failed.", e); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new RuntimeException("Can not diversify", e); + } + } + + public static final byte[] fillVisa2(byte[] kdd, KeyPurpose key) { + byte[] data = new byte[16]; + System.arraycopy(kdd, 0, data, 0, 2); + System.arraycopy(kdd, 4, data, 2, 4); + data[6] = (byte) 0xF0; + data[7] = key.getValue(); + System.arraycopy(kdd, 0, data, 8, 2); + System.arraycopy(kdd, 4, data, 10, 4); + data[14] = (byte) 0x0F; + data[15] = key.getValue(); + return data; + } + + // Unknown origin + public static final byte[] fillVisa(byte[] kdd, KeyPurpose key) { + byte[] data = new byte[16]; + System.arraycopy(kdd, 0, data, 0, 4); + System.arraycopy(kdd, 8, data, 4, 2); + data[6] = (byte) 0xF0; + data[7] = 0x01; + System.arraycopy(kdd, 0, data, 8, 4); + System.arraycopy(kdd, 8, data, 12, 2); + data[14] = (byte) 0x0F; + data[15] = 0x01; + return data; + } + + public static final byte[] fillEmv(byte[] kdd, KeyPurpose key) { + byte[] data = new byte[16]; + // 6 rightmost bytes of init update response (which is 10 bytes) + System.arraycopy(kdd, 4, data, 0, 6); + data[6] = (byte) 0xF0; + data[7] = key.getValue(); + System.arraycopy(kdd, 4, data, 8, 6); + data[14] = (byte) 0x0F; + data[15] = key.getValue(); + return data; + + } + + @Override + public int getVersion() { + return version; + } + + @Override + // data must be padded by caller + public byte[] encrypt(byte[] data) throws GeneralSecurityException { + if (scp == GPSecureChannel.SCP02) { + return GPCrypto.dek_encrypt_des(sessionKeys.get(KeyPurpose.DEK), data); + } else if (scp == GPSecureChannel.SCP01) { + return GPCrypto.dek_encrypt_des(cardKeys.get(KeyPurpose.DEK), data); + } else if (scp == GPSecureChannel.SCP03) { + return GPCrypto.dek_encrypt_aes(cardKeys.get(KeyPurpose.DEK), data); + } else throw new IllegalStateException("Unknown SCP version"); + } + + @Override + public byte[] encryptKey(GPCardKeys key, KeyPurpose p) throws GeneralSecurityException { + if (!(key instanceof PlaintextKeys)) + throw new IllegalArgumentException(getClass().getName() + " can only handle " + getClass().getName()); + PlaintextKeys other = (PlaintextKeys) key; + logger.debug("Encrypting {} value {} with {}", p, HexUtils.bin2hex(other.cardKeys.get(p)), HexUtils.bin2hex(cardKeys.get(KeyPurpose.DEK))); + switch (scp) { + case SCP01: + return GPCrypto.dek_encrypt_des(cardKeys.get(KeyPurpose.DEK), other.cardKeys.get(p)); + case SCP02: + logger.debug("Encrypting {} value {} with {}", p, HexUtils.bin2hex(other.cardKeys.get(p)), HexUtils.bin2hex(sessionKeys.get(KeyPurpose.DEK))); + return GPCrypto.dek_encrypt_des(sessionKeys.get(KeyPurpose.DEK), other.cardKeys.get(p)); + case SCP03: + byte[] otherkey = other.cardKeys.get(p); + // Pad with random + int n = otherkey.length % 16 + 1; + byte[] plaintext = new byte[n * otherkey.length]; + GPCrypto.random.nextBytes(plaintext); + System.arraycopy(otherkey, 0, plaintext, 0, otherkey.length); + // encrypt + return GPCrypto.dek_encrypt_aes(cardKeys.get(KeyPurpose.DEK), plaintext); + default: + throw new GPException("Illegal SCP"); + + } + } + + @Override + public GPSessionKeys getSessionKeys(byte[] kdd) { + // Calculate session keys (ENC-MAC-DEK[-RMAC]) + for (KeyPurpose p : KeyPurpose.cardKeys()) { + switch (scp) { + case SCP01: + sessionKeys.put(p, deriveSessionKeySCP01(cardKeys.get(p), p, kdd)); + break; + case SCP02: + sessionKeys.put(p, deriveSessionKeySCP02(cardKeys.get(p), p, kdd)); + if (p == KeyPurpose.MAC) { + sessionKeys.put(KeyPurpose.RMAC, deriveSessionKeySCP02(cardKeys.get(p), KeyPurpose.RMAC, kdd)); + } + break; + case SCP03: + sessionKeys.put(p, deriveSessionKeySCP03(cardKeys.get(p), p, kdd)); + if (p == KeyPurpose.MAC) { + sessionKeys.put(KeyPurpose.RMAC, deriveSessionKeySCP03(cardKeys.get(p), KeyPurpose.RMAC, kdd)); + } + break; + default: + throw new IllegalStateException("Illegal SCP"); + + } + } + return new GPSessionKeys(this, sessionKeys.get(KeyPurpose.ENC), sessionKeys.get(KeyPurpose.MAC), sessionKeys.get(KeyPurpose.RMAC)); + } + + @Override + public byte[] kcv(KeyPurpose p) { + byte[] k = cardKeys.get(p); + + if (scp == GPSecureChannel.SCP03) + return GPCrypto.kcv_aes(k); + else if (scp == GPSecureChannel.SCP01 || scp == GPSecureChannel.SCP02) + return GPCrypto.kcv_3des(k); + else { + if (k.length == 16) { + logger.warn("Don't know how to calculate KCV, defaulting to SCP02"); + return GPCrypto.kcv_3des(k); + } else { + logger.warn("Don't know how to calculate KCV, defaulting to SCP03"); + return GPCrypto.kcv_aes(k); + } + } + } + + public void setVersion(int version) { + this.version = version; + } + + private byte[] deriveSessionKeySCP01(byte[] cardKey, KeyPurpose p, byte[] kdd) { + if (p == KeyPurpose.DEK) + return cardKey; + + byte[] derivationData = new byte[16]; + System.arraycopy(kdd, 12, derivationData, 0, 4); + System.arraycopy(kdd, 0, derivationData, 4, 4); + System.arraycopy(kdd, 8, derivationData, 8, 4); + System.arraycopy(kdd, 4, derivationData, 12, 4); + + try { + Cipher cipher = Cipher.getInstance(GPCrypto.DES3_ECB_CIPHER); + cipher.init(Cipher.ENCRYPT_MODE, GPCrypto.des3key(cardKey)); + return cipher.doFinal(derivationData); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalStateException("Can not calculate session keys", e); + } catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { + throw new RuntimeException("Session key calculation failed", e); + } + } + + private byte[] deriveSessionKeySCP02(byte[] cardKey, KeyPurpose p, byte[] sequence) { + try { + Cipher cipher = Cipher.getInstance(GPCrypto.DES3_CBC_CIPHER); + byte[] derivationData = new byte[16]; + System.arraycopy(sequence, 0, derivationData, 2, 2); + System.arraycopy(SCP02_CONSTANTS.get(p), 0, derivationData, 0, 2); + cipher.init(Cipher.ENCRYPT_MODE, GPCrypto.des3key(cardKey), GPCrypto.iv_null_8); + return cipher.doFinal(derivationData); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalStateException("Session keys calculation failed.", e); + } catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) { + throw new RuntimeException("Session keys calculation failed.", e); + } + } + + private byte[] deriveSessionKeySCP03(byte[] cardKey, KeyPurpose p, byte[] kdd) { + if (p == KeyPurpose.DEK) { + return cardKey; + } + byte[] kdf = GPCrypto.scp03_kdf(cardKey, SCP03_CONSTANTS.get(p), kdd, cardKey.length * 8); + return kdf; + } + + + @Override + public GPCardKeys diversify(GPSecureChannel scp, byte[] kdd) { + this.scp = scp; + // Do nothing + if (diversifier == Diversification.NONE) + return this; + // Calculate per-card keys from master key(s), if needed + for (Map.Entry e : cardKeys.entrySet()) { + cardKeys.put(e.getKey(), diversify(e.getValue(), e.getKey(), kdd, diversifier)); + } + return this; + } + + + @Override + public String toString() { + String enc = HexUtils.bin2hex(cardKeys.get(KeyPurpose.ENC)); + String enc_kcv = HexUtils.bin2hex(kcv(KeyPurpose.ENC)); + + String mac = HexUtils.bin2hex(cardKeys.get(KeyPurpose.MAC)); + String mac_kcv = HexUtils.bin2hex(kcv(KeyPurpose.MAC)); + + String dek = HexUtils.bin2hex(cardKeys.get(KeyPurpose.DEK)); + String dek_kcv = HexUtils.bin2hex(kcv(KeyPurpose.DEK)); + + return String.format("ENC=%s (KCV: %s) MAC=%s (KCV: %s) DEK=%s (KCV: %s) for %s", enc, enc_kcv, mac, mac_kcv, dek, dek_kcv, scp); + } + + @Override + public int getID() { + return id; + } + + public void setDiversifier(Diversification diversifier) { + this.diversifier = diversifier; + } + + // diversification methods + public enum Diversification { + NONE, VISA2, EMV, KDF3 + } +} diff --git a/src/main/java/pro/javacard/gp/PythiaKeys.java b/library/src/main/java/pro/javacard/gp/PythiaKeys.java similarity index 93% rename from src/main/java/pro/javacard/gp/PythiaKeys.java rename to library/src/main/java/pro/javacard/gp/PythiaKeys.java index 66cf1ad1..08f0a4cb 100644 --- a/src/main/java/pro/javacard/gp/PythiaKeys.java +++ b/library/src/main/java/pro/javacard/gp/PythiaKeys.java @@ -44,11 +44,11 @@ private static PlaintextKeys fromHint(OracleHint hint) throws GPDataException { try { final PlaintextKeys r; if (hint.key != null && hint.algo != null) { - r = PlaintextKeys.fromMasterKey(new GPKey(HexUtils.hex2bin(hint.key), GPKey.Type.valueOf(hint.algo))); + r = PlaintextKeys.fromMasterKey(HexUtils.hex2bin(hint.key)); } else if (hint.mac != null && hint.enc != null && hint.kek != null && hint.algo != null) { - GPKey enc = new GPKey(HexUtils.hex2bin(hint.enc), GPKey.Type.valueOf(hint.algo)); - GPKey mac = new GPKey(HexUtils.hex2bin(hint.mac), GPKey.Type.valueOf(hint.algo)); - GPKey dek = new GPKey(HexUtils.hex2bin(hint.kek), GPKey.Type.valueOf(hint.algo)); + byte[] enc = HexUtils.hex2bin(hint.enc); + byte[] mac = HexUtils.hex2bin(hint.mac); + byte[] dek = HexUtils.hex2bin(hint.kek); r = PlaintextKeys.fromKeys(enc, mac, dek); } else { throw new GPDataException("Oracle does not know the keys :("); @@ -115,7 +115,7 @@ public X509Certificate[] getAcceptedIssuers() { X509Certificate c = (X509Certificate) cf.generateCertificate(cert); return new X509Certificate[]{c}; } - } catch (CertificateException|IOException e) { + } catch (CertificateException | IOException e) { // Ignore } return new X509Certificate[0]; @@ -126,7 +126,7 @@ public X509Certificate[] getAcceptedIssuers() { SSLSocketFactory factory = ssl.getSocketFactory(); HttpsURLConnection con = (HttpsURLConnection) url.openConnection(); con.setSSLSocketFactory(factory); - con.setRequestProperty("User-Agent", "GlobalPlatformPro/" + GlobalPlatform.getVersion()); + con.setRequestProperty("User-Agent", "GlobalPlatformPro/" + GPSession.getVersion()); OracleHint[] hints; try (InputStreamReader in = new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8)) { @@ -185,8 +185,8 @@ public X509Certificate[] getAcceptedIssuers() { private final static OracleHint makeDefault() { OracleHint DEFAULT = new OracleHint(); DEFAULT.aid = HexUtils.bin2hex(GPData.defaultISDBytes); - DEFAULT.key = HexUtils.bin2hex(GPData.defaultKeyBytes); - DEFAULT.algo = GPKey.Type.DES3.name(); + DEFAULT.key = HexUtils.bin2hex(PlaintextKeys.defaultKeyBytes); + DEFAULT.algo = GPKeyInfo.Type.DES3.name(); return DEFAULT; } diff --git a/src/main/java/pro/javacard/gp/SCP0102Wrapper.java b/library/src/main/java/pro/javacard/gp/SCP0102Wrapper.java similarity index 80% rename from src/main/java/pro/javacard/gp/SCP0102Wrapper.java rename to library/src/main/java/pro/javacard/gp/SCP0102Wrapper.java index 819517d8..0942f4b8 100644 --- a/src/main/java/pro/javacard/gp/SCP0102Wrapper.java +++ b/library/src/main/java/pro/javacard/gp/SCP0102Wrapper.java @@ -19,17 +19,23 @@ */ package pro.javacard.gp; +import apdu4j.CommandAPDU; +import apdu4j.ResponseAPDU; + import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; -import javax.smartcardio.CommandAPDU; -import javax.smartcardio.ResponseAPDU; +import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.GeneralSecurityException; +import java.security.Key; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.EnumSet; +import static pro.javacard.gp.GPCardKeys.KeyPurpose.*; + + class SCP0102Wrapper extends SecureChannelWrapper { private final ByteArrayOutputStream rMac = new ByteArrayOutputStream(); @@ -42,7 +48,7 @@ class SCP0102Wrapper extends SecureChannelWrapper { private boolean postAPDU = false; - SCP0102Wrapper(GPSessionKeyProvider sessionKeys, int scp, EnumSet securityLevel, byte[] icv, byte[] ricv, int bs) { + SCP0102Wrapper(GPSessionKeys sessionKeys, int scp, EnumSet securityLevel, byte[] icv, byte[] ricv, int bs) { this.blockSize = bs; this.sessionKeys = sessionKeys; this.icv = icv; @@ -62,22 +68,22 @@ private static byte setBits(byte b, byte mask) { public void setSCPVersion(int scp) { // Major version of wrapper this.scp = 2; - if (scp < GlobalPlatform.SCP_02_04) { + if (scp < GPSession.SCP_02_04) { this.scp = 1; } // modes - if ((scp == GlobalPlatform.SCP_01_15) || (scp == GlobalPlatform.SCP_02_14) || (scp == GlobalPlatform.SCP_02_15) || (scp == GlobalPlatform.SCP_02_1A) || (scp == GlobalPlatform.SCP_02_1B)) { + if ((scp == GPSession.SCP_01_15) || (scp == GPSession.SCP_02_14) || (scp == GPSession.SCP_02_15) || (scp == GPSession.SCP_02_1A) || (scp == GPSession.SCP_02_1B)) { icvEnc = true; } else { icvEnc = false; } - if ((scp == GlobalPlatform.SCP_01_05) || (scp == GlobalPlatform.SCP_01_15) || (scp == GlobalPlatform.SCP_02_04) || (scp == GlobalPlatform.SCP_02_05) || (scp == GlobalPlatform.SCP_02_14) || (scp == GlobalPlatform.SCP_02_15)) { + if ((scp == GPSession.SCP_01_05) || (scp == GPSession.SCP_01_15) || (scp == GPSession.SCP_02_04) || (scp == GPSession.SCP_02_05) || (scp == GPSession.SCP_02_14) || (scp == GPSession.SCP_02_15)) { preAPDU = true; } else { preAPDU = false; } - if ((scp == GlobalPlatform.SCP_02_0A) || (scp == GlobalPlatform.SCP_02_0B) || (scp == GlobalPlatform.SCP_02_1A) || (scp == GlobalPlatform.SCP_02_1B)) { + if ((scp == GPSession.SCP_02_0A) || (scp == GPSession.SCP_02_0B) || (scp == GPSession.SCP_02_1A) || (scp == GPSession.SCP_02_1B)) { postAPDU = true; } else { postAPDU = false; @@ -131,13 +137,16 @@ public CommandAPDU wrap(CommandAPDU command) throws GPException { if (icv == null) { icv = new byte[8]; } else if (icvEnc) { - Cipher c = null; + Cipher c; + byte[] key = sessionKeys.get(MAC); if (scp == 1) { c = Cipher.getInstance(GPCrypto.DES3_ECB_CIPHER); - c.init(Cipher.ENCRYPT_MODE, sessionKeys.getKeyFor(GPSessionKeyProvider.KeyPurpose.MAC).getKeyAs(GPKey.Type.DES3)); + Key k = GPCrypto.des3key(key); + c.init(Cipher.ENCRYPT_MODE, k); } else { c = Cipher.getInstance(GPCrypto.DES_ECB_CIPHER); - c.init(Cipher.ENCRYPT_MODE, sessionKeys.getKeyFor(GPSessionKeyProvider.KeyPurpose.MAC).getKeyAs(GPKey.Type.DES)); + Key k = new SecretKeySpec(GPCrypto.resizeDES(key, 8), "DES"); + c.init(Cipher.ENCRYPT_MODE, k); } // encrypts the future ICV ? icv = c.doFinal(icv); @@ -155,9 +164,9 @@ public CommandAPDU wrap(CommandAPDU command) throws GPException { t.write(origData); if (scp == 1) { - icv = GPCrypto.mac_3des(sessionKeys.getKeyFor(GPSessionKeyProvider.KeyPurpose.MAC), t.toByteArray(), icv); + icv = GPCrypto.mac_3des(sessionKeys.get(MAC), t.toByteArray(), icv); } else if (scp == 2) { - icv = GPCrypto.mac_des_3des(sessionKeys.getKeyFor(GPSessionKeyProvider.KeyPurpose.MAC), t.toByteArray(), icv); + icv = GPCrypto.mac_des_3des(sessionKeys.get(MAC), t.toByteArray(), icv); } if (postAPDU) { @@ -183,7 +192,8 @@ public CommandAPDU wrap(CommandAPDU command) throws GPException { newLc += t.size() - origData.length; Cipher c = Cipher.getInstance(GPCrypto.DES3_CBC_CIPHER); - c.init(Cipher.ENCRYPT_MODE, sessionKeys.getKeyFor(GPSessionKeyProvider.KeyPurpose.ENC).getKeyAs(GPKey.Type.DES3), GPCrypto.iv_null_8); + Key k = GPCrypto.des3key(sessionKeys.get(ENC)); + c.init(Cipher.ENCRYPT_MODE, k, GPCrypto.iv_null_8); newData = c.doFinal(t.toByteArray()); t.reset(); } @@ -223,7 +233,7 @@ public ResponseAPDU unwrap(ResponseAPDU response) throws GPException { rMac.write(response.getSW1()); rMac.write(response.getSW2()); - ricv = GPCrypto.mac_des_3des(sessionKeys.getKeyFor(GPSessionKeyProvider.KeyPurpose.RMAC), GPCrypto.pad80(rMac.toByteArray(), 8), ricv); + ricv = GPCrypto.mac_des_3des(sessionKeys.get(RMAC), GPCrypto.pad80(rMac.toByteArray(), 8), ricv); byte[] actualMac = new byte[8]; System.arraycopy(response.getData(), respLen, actualMac, 0, 8); diff --git a/src/main/java/pro/javacard/gp/SCP03Wrapper.java b/library/src/main/java/pro/javacard/gp/SCP03Wrapper.java similarity index 84% rename from src/main/java/pro/javacard/gp/SCP03Wrapper.java rename to library/src/main/java/pro/javacard/gp/SCP03Wrapper.java index 7a69e4b7..05cdb4f2 100644 --- a/src/main/java/pro/javacard/gp/SCP03Wrapper.java +++ b/library/src/main/java/pro/javacard/gp/SCP03Wrapper.java @@ -19,13 +19,13 @@ */ package pro.javacard.gp; +import apdu4j.CommandAPDU; import apdu4j.HexUtils; +import apdu4j.ResponseAPDU; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; -import javax.smartcardio.CommandAPDU; -import javax.smartcardio.ResponseAPDU; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.GeneralSecurityException; @@ -33,12 +33,15 @@ import java.util.Arrays; import java.util.EnumSet; +import static pro.javacard.gp.GPCardKeys.KeyPurpose.ENC; +import static pro.javacard.gp.GPCardKeys.KeyPurpose.RMAC; + class SCP03Wrapper extends SecureChannelWrapper { // Both are block size length byte[] chaining_value = new byte[16]; byte[] encryption_counter = new byte[16]; - SCP03Wrapper(GPSessionKeyProvider sessionKeys, int scp, EnumSet securityLevel, byte[] icv, byte[] ricv, int bs) { + SCP03Wrapper(GPSessionKeys sessionKeys, int scp, EnumSet securityLevel, byte[] icv, byte[] ricv, int bs) { this.sessionKeys = sessionKeys; this.blockSize = bs; // initialize chaining value. @@ -66,10 +69,10 @@ protected CommandAPDU wrap(CommandAPDU command) throws GPException { byte[] d = GPCrypto.pad80(command.getData(), 16); // Encrypt with S-ENC, after increasing the counter Cipher c = Cipher.getInstance(GPCrypto.AES_CBC_CIPHER); - c.init(Cipher.ENCRYPT_MODE, sessionKeys.getKeyFor(GPSessionKeyProvider.KeyPurpose.ENC).getKeyAs(GPKey.Type.AES), GPCrypto.iv_null_16); + c.init(Cipher.ENCRYPT_MODE, GPCrypto.aeskey(sessionKeys.get(ENC)), GPCrypto.iv_null_16); byte[] iv = c.doFinal(encryption_counter); // Now encrypt the data with S-ENC. - c.init(Cipher.ENCRYPT_MODE, sessionKeys.getKeyFor(GPSessionKeyProvider.KeyPurpose.ENC).getKeyAs(GPKey.Type.AES), new IvParameterSpec(iv)); + c.init(Cipher.ENCRYPT_MODE, GPCrypto.aeskey(sessionKeys.get(ENC)), new IvParameterSpec(iv)); data = c.doFinal(d); lc = data.length; } @@ -88,7 +91,7 @@ protected CommandAPDU wrap(CommandAPDU command) throws GPException { bo.write(GPUtils.encodeLcLength(lc)); bo.write(data); byte[] cmac_input = bo.toByteArray(); - byte[] cmac = GPCrypto.scp03_mac(sessionKeys.getKeyFor(GPSessionKeyProvider.KeyPurpose.MAC), cmac_input, 128); + byte[] cmac = GPCrypto.scp03_mac(sessionKeys.get(GPCardKeys.KeyPurpose.MAC), cmac_input, 128); // Set new chaining value System.arraycopy(cmac, 0, chaining_value, 0, chaining_value.length); // 8 bytes for actual mac @@ -96,20 +99,19 @@ protected CommandAPDU wrap(CommandAPDU command) throws GPException { } // Constructing new a new command APDU ensures that the coding of LC and NE is correct; especially for Extend Length APDUs CommandAPDU newAPDU = null; - + ByteArrayOutputStream newData = new ByteArrayOutputStream(); newData.write(data); if (mac) { newData.write(cmd_mac); } if (command.getNe() > 0) { - newAPDU = new CommandAPDU(cla, command.getINS(), command.getP1(), command.getP2(),newData.toByteArray(),command.getNe()); - } - else { - newAPDU = new CommandAPDU(cla, command.getINS(), command.getP1(), command.getP2(),newData.toByteArray()); + newAPDU = new CommandAPDU(cla, command.getINS(), command.getP1(), command.getP2(), newData.toByteArray(), command.getNe()); + } else { + newAPDU = new CommandAPDU(cla, command.getINS(), command.getP1(), command.getP2(), newData.toByteArray()); } return newAPDU; - + } catch (IOException e) { throw new RuntimeException("APDU wrapping failed", e); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { @@ -139,7 +141,7 @@ protected ResponseAPDU unwrap(ResponseAPDU response) throws GPException { byte[] cmac_input = bo.toByteArray(); - byte[] cmac = GPCrypto.scp03_mac(sessionKeys.getKeyFor(GPSessionKeyProvider.KeyPurpose.RMAC), cmac_input, 128); + byte[] cmac = GPCrypto.scp03_mac(sessionKeys.get(RMAC), cmac_input, 128); // 8 bytes for actual mac byte[] resp_mac = Arrays.copyOf(cmac, 8); @@ -154,15 +156,15 @@ protected ResponseAPDU unwrap(ResponseAPDU response) throws GPException { o.write(response.getSW2()); response = new ResponseAPDU(o.toByteArray()); } - if (renc && response.getData().length>0) { + if (renc && response.getData().length > 0) { // Encrypt with S-ENC, after changing the first byte of the counter - byte [] response_encryption_counter = Arrays.copyOf(encryption_counter, encryption_counter.length); + byte[] response_encryption_counter = Arrays.copyOf(encryption_counter, encryption_counter.length); response_encryption_counter[0] = (byte) 0x80; Cipher c = Cipher.getInstance(GPCrypto.AES_CBC_CIPHER); - c.init(Cipher.ENCRYPT_MODE, sessionKeys.getKeyFor(GPSessionKeyProvider.KeyPurpose.ENC).getKeyAs(GPKey.Type.AES), GPCrypto.iv_null_16); + c.init(Cipher.ENCRYPT_MODE, GPCrypto.aeskey(sessionKeys.get(ENC)), GPCrypto.iv_null_16); byte[] iv = c.doFinal(response_encryption_counter); // Now decrypt the data with S-ENC, with the new IV - c.init(Cipher.DECRYPT_MODE, sessionKeys.getKeyFor(GPSessionKeyProvider.KeyPurpose.ENC).getKeyAs(GPKey.Type.AES), new IvParameterSpec(iv)); + c.init(Cipher.DECRYPT_MODE, GPCrypto.aeskey(sessionKeys.get(ENC)), new IvParameterSpec(iv)); byte[] data = c.doFinal(response.getData()); ByteArrayOutputStream o = new ByteArrayOutputStream(); o.write(GPCrypto.unpad80(data)); diff --git a/src/main/java/pro/javacard/gp/SEAccessControl.java b/library/src/main/java/pro/javacard/gp/SEAccessControl.java similarity index 96% rename from src/main/java/pro/javacard/gp/SEAccessControl.java rename to library/src/main/java/pro/javacard/gp/SEAccessControl.java index 810c5a73..530b20a0 100644 --- a/src/main/java/pro/javacard/gp/SEAccessControl.java +++ b/library/src/main/java/pro/javacard/gp/SEAccessControl.java @@ -24,12 +24,11 @@ package pro.javacard.gp; import apdu4j.HexUtils; -import apdu4j.ISO7816; import com.payneteasy.tlv.*; import org.bouncycastle.util.Arrays; import pro.javacard.AID; -import javax.smartcardio.CardException; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.*; @@ -122,16 +121,12 @@ private static byte[] buildApduArDoData(final EventAccessRules rule, final byte[ } } - /** + /* * Parse REF_AR_DO object (p46 Secure Element Access Control v1.0). *

*

* 0xE2 | length | REF-DO | AR-DO *

- * - * @param refArDo REF_AR_DO data - * @return - * @throws GPDataException */ public static RefArDo parseRefArDo(final BerTlv refArDo) throws GPDataException { RefDo refDo = parseRefDo(refArDo.find(new BerTag(REF_DO))); @@ -139,16 +134,12 @@ public static RefArDo parseRefArDo(final BerTlv refArDo) throws GPDataException return new RefArDo(refDo, arDo); } - /** + /* * Parse REF_DO object (p46 Secure Element Access control v1.0). *

*

* 0xE1 | length | AID-REF-DO | Hash-REF-DO *

- * - * @param refDo - * @return - * @throws GPDataException */ public static RefDo parseRefDo(final BerTlv refDo) throws GPDataException { AidRefDo aidRefDo = parseAidRefDo(refDo.find(new BerTag(AID_REF_DO))); @@ -156,33 +147,25 @@ public static RefDo parseRefDo(final BerTlv refDo) throws GPDataException { return new RefDo(aidRefDo, hashRefDo); } - /** + /* * Parse AID_REF_DO object (p45 Secure Element Access Control v1.0). *

* 4F | length | AID - * - * @param aidRefDo - * @return - * @throws GPDataException */ public static AidRefDo parseAidRefDo(final BerTlv aidRefDo) throws GPDataException { return new AidRefDo(aidRefDo != null ? aidRefDo.getBytesValue() : new byte[]{}); } - /** + /* * Parse HASH_REF_DO (p46 Secure Element Access Control v1.0). *

* C1 | length | hash - * - * @param hashRefDo - * @return - * @throws GPDataException */ public static HashRefDo parseHashRefDo(final BerTlv hashRefDo) throws GPDataException { return new HashRefDo(hashRefDo != null ? hashRefDo.getBytesValue() : new byte[]{}); } - /** + /* * Parse AR_DO (p47 Secure Element Access Control v1.0) *

* E3 | length | APDU-AR-DO @@ -194,10 +177,6 @@ public static HashRefDo parseHashRefDo(final BerTlv hashRefDo) throws GPDataExce * OR *

* E3 | length | APDU-AR-DO | NFC-AR-DO - * - * @param arDo - * @return - * @throws GPDataException */ public static ArDo parseArDo(final BerTlv arDo) throws GPDataException { if (arDo != null) { @@ -208,14 +187,10 @@ public static ArDo parseArDo(final BerTlv arDo) throws GPDataException { return null; } - /** + /* * Parse APDU_AR_DO (p48 Secure Element Access Control v1.0). *

* D0 | length | 0x00 or 0x01 or APDU filter 1 | APDU filter n - * - * @param apduArDo - * @return - * @throws GPDataException */ public static ApduArDo parseApduArDo(final BerTlv apduArDo) throws GPDataException { if (apduArDo != null) { @@ -234,14 +209,10 @@ public static ApduArDo parseApduArDo(final BerTlv apduArDo) throws GPDataExcepti return null; } - /** + /* * Parse NFC_AR_DO (p49 Secure Element Access Control v1.0). *

* D1 | 01 | 0x00 or 0x01 - * - * @param nfcArDo - * @return - * @throws GPDataException */ public static NfcArDo parseNfcArDo(final BerTlv nfcArDo) throws GPDataException { if (nfcArDo != null) { @@ -255,10 +226,8 @@ public static NfcArDo parseNfcArDo(final BerTlv nfcArDo) throws GPDataException return null; } - /** + /* * Print ACR list response. - * - * @param acrList list of REF-AR-DO */ public static void printList(final List acrList) { if (acrList.size() == 0) { @@ -646,7 +615,7 @@ public void setCurrentIndex(int index) { } } - /** + /* * Parse access rule list response. */ public static class AcrListResponse { @@ -717,14 +686,14 @@ public static AcrListResponse fromBytes(final byte[] data) throws GPDataExceptio } public static class AcrListFetcher { - final GlobalPlatform gp; + final GPSession gp; - public AcrListFetcher(GlobalPlatform gp) { + public AcrListFetcher(GPSession gp) { this.gp = gp; } // Assumes a SD AID is selected - public byte[] get(AID araAid) throws CardException, GPException { + public byte[] get(AID araAid) throws IOException, GPException { byte[] result = new byte[0]; byte[] r; @@ -746,7 +715,7 @@ public byte[] get(AID araAid) throws CardException, GPException { return result; } - public byte[] get() throws CardException, GPException { + public byte[] get() throws IOException, GPException { return get(null); } } diff --git a/src/main/java/pro/javacard/gp/SEAccessControlUtility.java b/library/src/main/java/pro/javacard/gp/SEAccessControlUtility.java similarity index 75% rename from src/main/java/pro/javacard/gp/SEAccessControlUtility.java rename to library/src/main/java/pro/javacard/gp/SEAccessControlUtility.java index de8d4f84..cde19e41 100644 --- a/src/main/java/pro/javacard/gp/SEAccessControlUtility.java +++ b/library/src/main/java/pro/javacard/gp/SEAccessControlUtility.java @@ -23,28 +23,25 @@ */ package pro.javacard.gp; +import apdu4j.APDUBIBO; +import apdu4j.CommandAPDU; +import apdu4j.ResponseAPDU; import com.payneteasy.tlv.BerTlv; import com.payneteasy.tlv.BerTlvBuilder; import pro.javacard.AID; -import javax.smartcardio.*; +import java.io.IOException; /** * SE Access Control utility. */ public final class SEAccessControlUtility { - /** + /* * Send Access Control rule GET DATA. - * - * @param channel - * @param P1 - * @return - * @throws CardException - * @throws GPException */ - private static ResponseAPDU sendAcrGetData(final CardChannel channel, final byte P1) throws CardException, GPException { - CommandAPDU list = new CommandAPDU(GlobalPlatform.CLA_GP, GlobalPlatform.INS_GET_DATA, 0xFF, P1, 256); + private static ResponseAPDU sendAcrGetData(final APDUBIBO channel, final byte P1) throws IOException, GPException { + CommandAPDU list = new CommandAPDU(GPSession.CLA_GP, GPSession.INS_GET_DATA, 0xFF, P1, 256); ResponseAPDU response = channel.transmit(list); @@ -59,13 +56,10 @@ private static ResponseAPDU sendAcrGetData(final CardChannel channel, final byte return response; } - /** + /* * List access rules. - * - * @throws CardException - * @throws GPException */ - public static void acrList(final GlobalPlatform gp) throws CardException, GPException { + public static void acrList(final GPSession gp) throws IOException, GPException { try { gp.select(SEAccessControl.ACR_AID); @@ -84,29 +78,19 @@ public static void acrList(final GlobalPlatform gp) throws CardException, GPExce } } - /** + /* * Add an access rule. - * - * @param aid - * @param hash - * @param rules - * @throws CardException - * @throws GPException */ - public static void acrAdd(final GlobalPlatform gp, final AID araAid, final AID aid, final byte[] hash, final byte[] rules) throws CardException, GPException { + public static void acrAdd(final GPSession gp, final AID araAid, final AID aid, final byte[] hash, final byte[] rules) throws IOException, GPException { SEAccessControl.RefArDo refArDo = new SEAccessControl.RefArDo(aid, hash, rules); SEAccessControl.StoreArDo storeArDo = new SEAccessControl.StoreArDo(refArDo); acrStore(gp, araAid, storeArDo.toTlv()); } - /** + /* * Send store data for access rule. - * - * @param data TLV data - * @throws CardException - * @throws GPException */ - public static void acrStore(final GlobalPlatform gp, final AID araAid, final BerTlv data) throws CardException, GPException { + public static void acrStore(final GPSession gp, final AID araAid, final BerTlv data) throws IOException, GPException { try { //0x90 is for getting BER-TLV data (Secure Element Access Control v1.0 p36) gp.personalizeSingle(araAid, new BerTlvBuilder().addBerTlv(data).buildArray(), (byte) 0x90); @@ -119,15 +103,10 @@ public static void acrStore(final GlobalPlatform gp, final AID araAid, final Ber } } - /** + /* * Delete an access rule by AID/HASH. - * - * @param aid - * @param hash - * @throws CardException - * @throws GPException */ - public static void acrDelete(final GlobalPlatform gp, final AID araAid, final AID aid, final byte[] hash) throws CardException, GPException { + public static void acrDelete(final GPSession gp, final AID araAid, final AID aid, final byte[] hash) throws IOException, GPException { BerTlv request; if (hash != null) { diff --git a/src/main/java/pro/javacard/gp/SecureChannelParameters.java b/library/src/main/java/pro/javacard/gp/SecureChannelParameters.java similarity index 76% rename from src/main/java/pro/javacard/gp/SecureChannelParameters.java rename to library/src/main/java/pro/javacard/gp/SecureChannelParameters.java index fff538af..c37aca24 100644 --- a/src/main/java/pro/javacard/gp/SecureChannelParameters.java +++ b/library/src/main/java/pro/javacard/gp/SecureChannelParameters.java @@ -29,14 +29,14 @@ public class SecureChannelParameters { private AID aid; - private GPSessionKeyProvider sessionKeyProvider; + private GPCardKeys cardKeys; public Optional getAID() { return Optional.ofNullable(aid); } - public GPSessionKeyProvider getSessionKeys() { - return sessionKeyProvider; + public GPCardKeys getCardKeys() { + return cardKeys; } public static Optional fromEnvironment() { @@ -47,12 +47,12 @@ public static Optional fromKeyValuePairs(Map securityLevel) { - mac = securityLevel.contains(GlobalPlatform.APDUMode.MAC); - enc = securityLevel.contains(GlobalPlatform.APDUMode.ENC); - rmac = securityLevel.contains(GlobalPlatform.APDUMode.RMAC); - renc = securityLevel.contains(GlobalPlatform.APDUMode.RENC); + public void setSecurityLevel(EnumSet securityLevel) { + mac = securityLevel.contains(GPSession.APDUMode.MAC); + enc = securityLevel.contains(GPSession.APDUMode.ENC); + rmac = securityLevel.contains(GPSession.APDUMode.RMAC); + renc = securityLevel.contains(GPSession.APDUMode.RENC); } protected int getBlockSize() { diff --git a/src/main/resources/pro/javacard/gp/javacard.pro.pem b/library/src/main/resources/pro/javacard/gp/javacard.pro.pem similarity index 100% rename from src/main/resources/pro/javacard/gp/javacard.pro.pem rename to library/src/main/resources/pro/javacard/gp/javacard.pro.pem diff --git a/src/main/resources/pro/javacard/gp/letsencrypt.pem b/library/src/main/resources/pro/javacard/gp/letsencrypt.pem similarity index 100% rename from src/main/resources/pro/javacard/gp/letsencrypt.pem rename to library/src/main/resources/pro/javacard/gp/letsencrypt.pem diff --git a/src/test/java/pro/javacard/gp/TestDMTokenGenerator.java b/library/src/test/java/pro/javacard/gp/TestDMTokenGenerator.java similarity index 58% rename from src/test/java/pro/javacard/gp/TestDMTokenGenerator.java rename to library/src/test/java/pro/javacard/gp/TestDMTokenGenerator.java index d138fccf..adc19b81 100644 --- a/src/test/java/pro/javacard/gp/TestDMTokenGenerator.java +++ b/library/src/test/java/pro/javacard/gp/TestDMTokenGenerator.java @@ -1,44 +1,41 @@ package pro.javacard.gp; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import javax.smartcardio.CommandAPDU; +import apdu4j.CommandAPDU; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; import java.io.File; import java.io.FileInputStream; import java.security.PrivateKey; -import static pro.javacard.gp.GlobalPlatform.CLA_GP; -import static pro.javacard.gp.GlobalPlatform.INS_INSTALL; +import static pro.javacard.gp.GPSession.CLA_GP; +import static pro.javacard.gp.GPSession.INS_INSTALL; public class TestDMTokenGenerator { private PrivateKey key; - @Before - public void setUp() { + @BeforeClass + public void setUp() throws Exception { try (FileInputStream fin = new FileInputStream(new File("src/test/resources/test-private.pem"))) { key = GPCrypto.pem2PrivateKey(fin); - } catch (Exception e) { - e.printStackTrace(); } } @Test - public void testApplyToken() { + public void testApplyToken() throws Exception { CommandAPDU command = new CommandAPDU(CLA_GP, INS_INSTALL, 0x02, 0x00, new byte[]{0}); DMTokenGenerator dmHandler = new DMTokenGenerator(key); command = dmHandler.applyToken(command); - Assert.assertTrue(command.getData().length > 1); + Assert.assertTrue(command.getData().length > 1); // FIXME: test for value of token content and signature } @Test - public void testApplyEmptyToken() { + public void testApplyEmptyToken() throws Exception { CommandAPDU command = new CommandAPDU(CLA_GP, INS_INSTALL, 0x02, 0x00, new byte[]{0}); DMTokenGenerator dmHandler = new DMTokenGenerator(null); command = dmHandler.applyToken(command); - Assert.assertArrayEquals(command.getData(), new byte[]{0, 0}); + Assert.assertEquals(command.getData(), new byte[]{0, 0}); } } diff --git a/src/test/java/pro/javacard/gp/TestParseTags.java b/library/src/test/java/pro/javacard/gp/TestParseTags.java similarity index 83% rename from src/test/java/pro/javacard/gp/TestParseTags.java rename to library/src/test/java/pro/javacard/gp/TestParseTags.java index d04f8144..d0c3d187 100644 --- a/src/test/java/pro/javacard/gp/TestParseTags.java +++ b/library/src/test/java/pro/javacard/gp/TestParseTags.java @@ -1,11 +1,11 @@ package pro.javacard.gp; import apdu4j.HexUtils; -import org.junit.Assert; -import org.junit.Ignore; -import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Ignore; +import org.testng.annotations.Test; import pro.javacard.AID; import java.util.List; @@ -17,7 +17,7 @@ public class TestParseTags { public void testSomething() throws Exception { byte[] data = HexUtils.hex2bin("E3464F08A0000000030000009F700101C5039EFE80CF0140CF0141CF0142CF0143CF0180CF0181CF0182CF0183C40BD276000005AAFFCAFE0001CE020001CC08A000000003000000"); GPRegistry reg = new GPRegistry(); - reg.parse(0x80, data, GPRegistryEntry.Kind.IssuerSecurityDomain, GlobalPlatform.GPSpec.GP22); + reg.parse(0x80, data, GPRegistryEntry.Kind.IssuerSecurityDomain, GPSession.GPSpec.GP22); } @Test @@ -56,22 +56,22 @@ public void testOracle() throws Exception { @Test public void testRSAKeyTemplate() throws Exception { byte[] t = HexUtils.hex2bin("E020C00401018820C00402018820C00403018820C0060170A180A003C00401718010"); - List kl = GPData.get_key_template_list(t); - GPData.pretty_print_key_template(kl, System.out); + List kl = GPKeyInfo.parseTemplate(t); + GPKeyInfo.print(kl, System.out); } @Test public void testExtendedRSAKeyTemplate() throws Exception { byte[] t = HexUtils.hex2bin("E021C00401018010C00402018010C00403018010C00D0173FFA10080A0000301840100"); - List kl = GPData.get_key_template_list(t); - GPData.pretty_print_key_template(kl, System.out); + List kl = GPKeyInfo.parseTemplate(t); + GPKeyInfo.print(kl, System.out); } @Test public void testExtendedKeyTypeTemplateWithZeroLengths() throws Exception { byte[] t = HexUtils.hex2bin("E081B0C00A0120FF80001001000100C00A0220FF80001001000100C00A0320FF80001001000100C00A0101FF80001001000100C00A0201FF80001001000100C00A0301FF80001001000100C00A0102FF88001001000100C00E0202FF880010FF10000101000100C00A0302FF88001001000100C00A0103FF88001001000100C00E0203FF880010FF10000101000100C00A0303FF88001001000100C00A1403FF85001001000100C00A1503FF88001001000100"); - List kl = GPData.get_key_template_list(t); - GPData.pretty_print_key_template(kl, System.out); + List kl = GPKeyInfo.parseTemplate(t); + GPKeyInfo.print(kl, System.out); } @Test @@ -88,22 +88,23 @@ public void testCPLCDateParse() throws Exception { public void testParseISD() throws Exception { byte[] r = HexUtils.hex2bin("E3144F07A00000015100009F700107C50180EA028000"); GPRegistry g = new GPRegistry(); - g.parse(0x80, r, GPRegistryEntry.Kind.IssuerSecurityDomain, GlobalPlatform.GPSpec.GP22); + g.parse(0x80, r, GPRegistryEntry.Kind.IssuerSecurityDomain, GPSession.GPSpec.GP22); + GPRegistryEntry isd = g.getISD().orElseThrow(() -> new Exception("No ISD")); Assert.assertEquals(1, g.allAIDs().size()); - Assert.assertEquals(AID.fromString("A0000001510000"), g.getISD().getAID()); - Assert.assertEquals(1, g.getISD().getPrivileges().size()); - Assert.assertTrue(g.getISD().getPrivileges().has(GPRegistryEntry.Privilege.SecurityDomain)); - Assert.assertEquals(GPData.initializedStatus, g.getISD().getLifeCycle()); + Assert.assertEquals(AID.fromString("A0000001510000"), isd.getAID()); + Assert.assertEquals(1, isd.getPrivileges().size()); + Assert.assertTrue(isd.getPrivileges().has(GPRegistryEntry.Privilege.SecurityDomain)); + Assert.assertEquals(GPData.initializedStatus, isd.getLifeCycle()); } - @Test(expected = GPDataException.class) + @Test(expectedExceptions = GPDataException.class) public void testCPLCDateParseInvalid() throws Exception { byte[] b = HexUtils.hex2bin("1410"); GPData.CPLC.toDate(b); } - @Test(expected = GPDataException.class) + @Test(expectedExceptions = GPDataException.class) public void testCPLCTagless() throws Exception { byte[] b = HexUtils.hex2bin("FF401AE218E116C1144434050D9648B771CB3500D5398D36CE3F1C23A4"); //b = Arrays.copyOf(b, 0x2A); diff --git a/src/test/java/pro/javacard/gp/TestSCP03.java b/library/src/test/java/pro/javacard/gp/TestSCP03.java similarity index 78% rename from src/test/java/pro/javacard/gp/TestSCP03.java rename to library/src/test/java/pro/javacard/gp/TestSCP03.java index 77718e9b..d092bd84 100644 --- a/src/test/java/pro/javacard/gp/TestSCP03.java +++ b/library/src/test/java/pro/javacard/gp/TestSCP03.java @@ -1,12 +1,12 @@ package pro.javacard.gp; import apdu4j.HexUtils; -import org.junit.Assert; -import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Test; -import static pro.javacard.gp.GPSessionKeyProvider.KeyPurpose.ENC; +import static pro.javacard.gp.GPCardKeys.KeyPurpose.ENC; import static pro.javacard.gp.PlaintextKeys.SCP03_KDF_CONSTANTS; @@ -26,6 +26,6 @@ public void testKDF3() { final byte[] kv = GPCrypto.scp03_kdf(master, blocka, blockb, master.length); System.out.println("ENC: " + HexUtils.bin2hex(kv)); - Assert.assertArrayEquals(HexUtils.hex2bin("9AAC5D0B3601F89438A0D9D0B6B256CFB47E6462DFA5228D3420C4AC7C224781"), kv); + Assert.assertEquals(HexUtils.hex2bin("9AAC5D0B3601F89438A0D9D0B6B256CFB47E6462DFA5228D3420C4AC7C224781"), kv); } } diff --git a/src/test/java/pro/javacard/gp/TestSEAC.java b/library/src/test/java/pro/javacard/gp/TestSEAC.java similarity index 86% rename from src/test/java/pro/javacard/gp/TestSEAC.java rename to library/src/test/java/pro/javacard/gp/TestSEAC.java index 7feac3dc..217513c7 100644 --- a/src/test/java/pro/javacard/gp/TestSEAC.java +++ b/library/src/test/java/pro/javacard/gp/TestSEAC.java @@ -1,8 +1,7 @@ package pro.javacard.gp; import apdu4j.HexUtils; -import org.junit.Test; -import pro.javacard.gp.SEAccessControl; +import org.testng.annotations.Test; public class TestSEAC { diff --git a/src/test/resources/test-private.pem b/library/src/test/resources/test-private.pem similarity index 100% rename from src/test/resources/test-private.pem rename to library/src/test/resources/test-private.pem diff --git a/pom.xml b/pom.xml index 932c6b91..7e296e0d 100644 --- a/pom.xml +++ b/pom.xml @@ -1,13 +1,15 @@ - + 4.0.0 com.github.martinpaljak javacard - 18.10.04 + 19.05.09 - globalplatformpro - jar + 19.05.16 + gppro + pom GlobalPlatformPro https://github.com/martinpaljak/GlobalPlatformPro Manage applets and keys on JavaCard-s like a pro @@ -23,70 +25,11 @@ https://github.com/martinpaljak/GlobalPlatformPro scm:git:git@github.com:martinpaljak/globalplatformpro.git - - - - com.github.martinpaljak - apdu4j - 18.10.04 - - - - com.github.martinpaljak - capfile - 18.10.04 - - - - com.google.code.gson - gson - 2.8.4 - - - - org.apache.httpcomponents - httpclient - ${httpclient.version} - - - - org.slf4j - slf4j-simple - ${slf4j.version} - true - - - - org.slf4j - slf4j-api - ${slf4j.version} - - - - org.bouncycastle - bcpkix-jdk15on - ${bc.version} - - - - net.sf.jopt-simple - jopt-simple - ${jopt.version} - true - - - - com.payneteasy - ber-tlv - ${bertlv.version} - - - junit - junit - 4.12 - test - - + + tool + library + + @@ -115,41 +58,18 @@ target/generated-resources/pro/javacard/gp/pro_version.txt -c - [ "x$GPPRO_VERSION" = "x" ] && git describe --tags --always --long --dirty || echo "$GPPRO_VERSION" + [ "x$GPPRO_VERSION" = "x" ] && git describe --tags --always --long --dirty || + echo "$GPPRO_VERSION" + + + + - org.apache.maven.plugins - maven-shade-plugin - 3.0.0 - - - package - - shade - - - gp - - - pro.javacard.gp.GPTool - - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - META-INF/maven/** - - - - - - + io.github.zlika + reproducible-build-maven-plugin diff --git a/src/main/java/pro/javacard/gp/DMTokenGenerator.java b/src/main/java/pro/javacard/gp/DMTokenGenerator.java deleted file mode 100644 index 17a8efc3..00000000 --- a/src/main/java/pro/javacard/gp/DMTokenGenerator.java +++ /dev/null @@ -1,80 +0,0 @@ -package pro.javacard.gp; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.smartcardio.CommandAPDU; -import java.io.ByteArrayOutputStream; -import java.security.PrivateKey; -import java.security.Signature; - -import static pro.javacard.gp.GlobalPlatform.INS_DELETE; - -public class DMTokenGenerator { - private static final Logger logger = LoggerFactory.getLogger(DMTokenGenerator.class); - private static final String acceptedSignatureAlgorithm = "SHA1withRSA"; - - private PrivateKey key; - - public DMTokenGenerator(PrivateKey key) { - this.key = key; - } - - public CommandAPDU applyToken(CommandAPDU apdu) { - ByteArrayOutputStream newData = new ByteArrayOutputStream(); - - try { - newData.write(apdu.getData()); - if (apdu.getINS() == INS_DELETE || apdu.getINS() == (INS_DELETE & 255)) { - // See GP 2.3.1 Table 11-23 - logger.debug("Adding tag 0x9E before Delete Token"); - newData.write(0x9E); - } - if (key == null) { - logger.debug("No private key for token generation provided"); - newData.write(0); //Token length - } else { - logger.debug("Using private key for token generation (" + acceptedSignatureAlgorithm + ")"); - byte[] token = calculateToken(apdu, key); - newData.write(token.length); - newData.write(token); - } - return new CommandAPDU(apdu.getCLA(), apdu.getINS(), apdu.getP1(), apdu.getP2(), newData.toByteArray()); - } catch (Exception e) { - throw new RuntimeException("Could not add DM token to constructed APDU", e); - } - } - - private static byte[] calculateToken(CommandAPDU apdu, PrivateKey key) { - return signData(key, getTokenData(apdu)); - } - - private static byte[] getTokenData(CommandAPDU apdu) { - try { - ByteArrayOutputStream bo = new ByteArrayOutputStream(); - bo.write(apdu.getP1()); - bo.write(apdu.getP2()); - bo.write(apdu.getData().length); - bo.write(apdu.getData()); - return bo.toByteArray(); - } catch (Exception e) { - throw new RuntimeException("Could not get P1/P2 or data for token calculation", e); - } - } - - private static byte[] signData(PrivateKey privateKey, byte[] apduData) { - try { - Signature signature = Signature.getInstance(acceptedSignatureAlgorithm); - signature.initSign(privateKey); - signature.update(apduData); - return signature.sign(); - } catch (Exception e) { - throw new RuntimeException("Could not create signature with instance " + acceptedSignatureAlgorithm, e); - } - } - - public boolean hasKey() { - return key != null; - } - -} diff --git a/src/main/java/pro/javacard/gp/GPKey.java b/src/main/java/pro/javacard/gp/GPKey.java deleted file mode 100644 index 5f79922b..00000000 --- a/src/main/java/pro/javacard/gp/GPKey.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * GlobalPlatformPro - GlobalPlatform tool - * - * Copyright (C) 2015-2017 Martin Paljak, martin@martinpaljak.net - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3.0 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ -package pro.javacard.gp; - -import apdu4j.HexUtils; - -import javax.crypto.spec.SecretKeySpec; -import java.security.Key; -import java.util.Arrays; - -// Encapsulates a plaintext symmetric key used with GlobalPlatform -public final class GPKey { - private Type type; - private int version = 0; // 1..7f - private int id = -1; // 0..7f - private int length = -1; - private transient byte[] bytes = null; - - // Create a key of given type and given bytes bytes - public GPKey(byte[] v, Type type) { - if (v.length != 16 && v.length != 24 && v.length != 32) - throw new IllegalArgumentException("A valid key must be 16/24/32 bytes long"); - this.bytes = Arrays.copyOf(v, v.length); - this.length = v.length; - this.type = type; - } - - // Raw key, that can be interpreted in any way. - public GPKey(byte[] key) { - this(key, Type.RAW); - } - - // Creates a new key with a new version and id, based on key type and bytes of an existing key - public GPKey(int version, int id, GPKey other) { - this(other.bytes, other.type); - this.version = version; - this.id = id; - } - - // Called when parsing KeyInfo template, no values present - public GPKey(int version, int id, int length, int type) { - this.version = version; - this.id = id; - this.length = length; - // FIXME: these values should be encapsulated somewhere - // FIXME: 0x81 is actually reserved according to GP - // GP 2.2.1 11.1.8 Key Type Coding - if (type == 0x80 || type == 0x81 || type == 0x82) { - this.type = Type.DES3; - } else if (type == 0x88) { - this.type = Type.AES; - } else if (type == 0xA1 || type == 0xA0) { - this.type = Type.RSAPUB; - } else if (type == 0x85) { - this.type = Type.PSK; - } else { - throw new UnsupportedOperationException(String.format("Only AES, 3DES, PSK and RSA public keys are supported currently: 0x%02X", type)); - } - } - - // Do shuffling as necessary - private static byte[] resizeDES(byte[] key, int length) { - if (length == 24) { - byte[] key24 = new byte[24]; - System.arraycopy(key, 0, key24, 0, 16); - System.arraycopy(key, 0, key24, 16, 8); - return key24; - } else { - byte[] key8 = new byte[8]; - System.arraycopy(key, 0, key8, 0, 8); - return key8; - } - } - - public int getID() { - return id; - } - - public int getVersion() { - return version; - } - - public byte[] getBytes() { - return Arrays.copyOf(bytes, bytes.length); - } - - public int getLength() { - return length; - } - - public Type getType() { - return type; - } - - // Returns a Java key, usable in Ciphers - // Only trick here is the size fiddling for DES - public Key getKeyAs(Type type) { - if (type == Type.DES) { - return new SecretKeySpec(resizeDES(bytes, 8), "DES"); - } else if (type == Type.DES3) { - return new SecretKeySpec(resizeDES(bytes, 24), "DESede"); - } else if (type == Type.AES) { - return new SecretKeySpec(bytes, "AES"); - } - throw new IllegalArgumentException("Can only create DES/3DES/AES keys"); - } - - public String toString() { - StringBuffer s = new StringBuffer(); - s.append("type=" + type); - if (version >= 1 && version <= 0x7f) - s.append(" version=" + String.format("%d (0x%02X)", version, version)); - if (id >= 0 && id <= 0x7F) - s.append(" id=" + String.format("%d (0x%02X)", id, id)); - if (bytes != null) - s.append(" bytes=" + HexUtils.bin2hex(bytes)); - else - s.append(" len=" + length); - byte[] kcv = getKCV(); - if (kcv.length > 0) { - s.append(" kcv=" + HexUtils.bin2hex(getKCV())); - } - return s.toString(); - } - - public byte[] getKCV() { - if (type == Type.DES3) { - return GPCrypto.kcv_3des(this); - } else if (type == Type.AES) { - return GPCrypto.scp03_key_check_value(this); - } else { - return new byte[0]; - } - } - - // Change the type of a RAW key - public void become(Type t) { - if (type != Type.RAW) - throw new IllegalStateException("Only RAW keys can become a new type"); - type = t; - } - - public enum Type { - RAW, DES, DES3, AES, RSAPUB, PSK; - - @Override - public String toString() { - if (this.name().equals("RSAPUB")) - return "RSA"; - return super.toString(); - } - } -} diff --git a/src/main/java/pro/javacard/gp/GPRegistry.java b/src/main/java/pro/javacard/gp/GPRegistry.java deleted file mode 100644 index 1659d3d7..00000000 --- a/src/main/java/pro/javacard/gp/GPRegistry.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * gpj - Global Platform for Java SmartCardIO - * - * Copyright (C) 2009 Wojciech Mostowski, woj@cs.ru.nl - * Copyright (C) 2009 Francois Kooman, F.Kooman@student.science.ru.nl - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3.0 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - */ - -package pro.javacard.gp; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; - -import com.payneteasy.tlv.*; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import pro.javacard.AID; -import pro.javacard.gp.GPRegistryEntry.Kind; -import pro.javacard.gp.GPRegistryEntry.Privilege; -import pro.javacard.gp.GPRegistryEntry.Privileges; -import pro.javacard.gp.GlobalPlatform.GPSpec; - - - -/** - * Convenience class managing a vector of {@link GPRegistryEntry - * AIDRegistryEntries} with search functionality. - * - * Implements {@code Iterable { - private static final Logger logger = LoggerFactory.getLogger(GPRegistry.class); - boolean tags = true; // XXX (visibility) true if newer tags format should be used for parsing, false otherwise - LinkedHashMap entries = new LinkedHashMap<>(); - - /** - * Add one entry to this registry. - * - * @param entry - */ - public void add(GPRegistryEntry entry) { - // "fix" the kind at a single location. - if (entry instanceof GPRegistryEntryApp) { - GPRegistryEntryApp app = (GPRegistryEntryApp) entry; - if (app.getPrivileges().has(Privilege.SecurityDomain) && entry.getType() == Kind.Application) { - entry.setType(Kind.SecurityDomain); - } - } - // XXX Legacy, combined with logic in GlobalPlatform.getStatus() - GPRegistryEntry existing = entries.get(entry.getAID()); - if (existing != null && existing.getType() != entry.getType()) { - // OP201 cards list the ISD AID as load file. - // FIXME: different types with same AID. - logger.warn("Duplicate entry! " + existing + " vs " + entry); - return; - } - entries.put(entry.getAID(), entry); - } - - /** - * Returns an iterator that iterates over all entries in this registry. - * - * @return iterator - */ - public Iterator iterator() { - return entries.values().iterator(); - } - - - /** - * Returns a list of all packages in this registry. - * - * @return a list of all packages - */ - public List allPackages() { - List res = new ArrayList(); - for (GPRegistryEntry e : entries.values()) { - if (e.isPackage()) { - res.add((GPRegistryEntryPkg)e); - } - } - return res; - } - - public List allPackageAIDs() { - List res = new ArrayList(); - for (GPRegistryEntry e : entries.values()) { - if (e.isPackage()) { - res.add(e.getAID()); - } - } - return res; - } - public List allAppletAIDs() { - List res = new ArrayList(); - for (GPRegistryEntry e : entries.values()) { - if (e.isApplet()) { - res.add(e.getAID()); - } - } - return res; - } - - public List allAIDs() { - List res = new ArrayList(); - for (GPRegistryEntry e : entries.values()) { - res.add(e.getAID()); - } - return res; - } - - public GPRegistryEntryApp getDomain(AID aid) { - for (GPRegistryEntryApp e : allDomains()) { - if (e.aid.equals(aid)) - return e; - } - return null; - } - - /** - * Returns a list of all applets in this registry. - * - * @return a list of all applets - */ - public List allApplets() { - List res = new ArrayList(); - for (GPRegistryEntry e : entries.values()) { - if (e.isApplet()) { - res.add((GPRegistryEntryApp)e); - } - } - return res; - } - - public List allDomains() { - List res = new ArrayList<>(); - for (GPRegistryEntry e : entries.values()) { - if (e.isDomain()) { - res.add((GPRegistryEntryApp)e); - } - } - return res; - } - - public AID getDefaultSelectedAID() { - for (GPRegistryEntryApp e : allApplets()) { - if (e.getPrivileges().has(Privilege.CardReset)) { - return e.getAID(); - } - } - return null; - } - - public AID getDefaultSelectedPackageAID() { - AID defaultAID = getDefaultSelectedAID(); - if (defaultAID != null) { - for (GPRegistryEntryPkg e : allPackages()) { - if (e.getModules().contains(defaultAID)) - return e.getAID(); - } - // Did not get a hit. Loop packages and look for prefixes - for (GPRegistryEntryPkg e : allPackages()) { - if (defaultAID.toString().startsWith(e.getAID().toString())) - return e.getAID(); - } - } - return null; - } - - // Shorthand - public GPRegistryEntryApp getISD() { - for (GPRegistryEntryApp a: allDomains()) { - if (a.getType() == Kind.IssuerSecurityDomain) { - return a; - } - } - // Could happen if the registry is a view from SSD - return null; - } - - private void populate_legacy(int p1, byte[] data, Kind type, GPSpec spec) throws GPDataException { - int offset = 0; - try { - while (offset < data.length) { - int len = data[offset++]; - AID aid = new AID(data, offset, len); - offset += len; - int lifecycle = (data[offset++] & 0xFF); - byte privileges = data[offset++]; - - if (type == Kind.IssuerSecurityDomain || type == Kind.Application) { - GPRegistryEntryApp app = new GPRegistryEntryApp(); - app.setType(type); - app.setAID(aid); - app.setPrivileges(Privileges.fromByte(privileges)); - app.setLifeCycle(lifecycle); - add(app); - } else if (type == Kind.ExecutableLoadFile) { - if (privileges != 0x00) { - throw new GPDataException("Privileges of Load File is not 0x00"); - } - GPRegistryEntryPkg pkg = new GPRegistryEntryPkg(); - pkg.setAID(aid); - pkg.setLifeCycle(lifecycle); - pkg.setType(type); - // Modules TODO: remove - if (spec != GPSpec.OP201 && p1 != 0x20) { - int num = data[offset++]; - for (int i = 0; i < num; i++) { - len = data[offset++] & 0xFF; - aid = new AID(data, offset, len); - offset += len; - pkg.addModule(aid); - } - } - add(pkg); - } - } - } catch (ArrayIndexOutOfBoundsException e) { - throw new GPDataException("Invalid response to GET STATUS", e); - } - } - - private void populate_tags(byte[] data, Kind type) throws GPDataException { - - BerTlvParser parser = new BerTlvParser(); - BerTlvs tlvs = parser.parse(data); - GPUtils.trace_tlv(data, logger); - - for (BerTlv t: tlvs.findAll(new BerTag(0xE3))) { - GPRegistryEntryApp app = new GPRegistryEntryApp(); - GPRegistryEntryPkg pkg = new GPRegistryEntryPkg(); - if (t.isConstructed()) { - BerTlv aid = t.find(new BerTag(0x4f)); - if (aid != null) { - AID aidv = new AID(aid.getBytesValue()); - app.setAID(aidv); - pkg.setAID(aidv); - } - BerTlv lifecycletag = t.find(new BerTag(0x9F, 0x70)); - if (lifecycletag != null) { - app.setLifeCycle(lifecycletag.getBytesValue()[0] & 0xFF); - pkg.setLifeCycle(lifecycletag.getBytesValue()[0] & 0xFF); - } - - BerTlv privstag = t.find(new BerTag(0xC5)); - if (privstag != null) { - Privileges privs = Privileges.fromBytes(privstag.getBytesValue()); - app.setPrivileges(privs); - } - for (BerTlv cf: t.findAll(new BerTag(0xCF))) { - logger.debug("CF=" + cf.getHexValue() + " for " + app.aid); - // FIXME: how to expose? - } - - BerTlv loadfiletag = t.find(new BerTag(0xC4)); - if (loadfiletag != null) { - app.setLoadFile(new AID(loadfiletag.getBytesValue())); - } - BerTlv versiontag = t.find(new BerTag(0xCE)); - if (versiontag != null) { - pkg.setVersion(versiontag.getBytesValue()); - } - - for (BerTlv lf: t.findAll(new BerTag(0x84))) { - pkg.addModule(new AID(lf.getBytesValue())); - } - - BerTlv domaintag = t.find(new BerTag(0xCC)); - if (domaintag != null) { - app.setDomain(new AID(domaintag.getBytesValue())); - pkg.setDomain(new AID(domaintag.getBytesValue())); - } - } - - // Construct entry - if (type == Kind.ExecutableLoadFile) { - pkg.setType(type); - add(pkg); - } else { - app.setType(type); - add(app); - } - } - } - - // FIXME: this is ugly - public void parse(int p1, byte[] data, Kind type, GPSpec spec) throws GPDataException { - if (tags) { - populate_tags(data, type); - } else { - populate_legacy(p1, data, type, spec); - } - } -} diff --git a/src/main/java/pro/javacard/gp/GPRegistryEntryApp.java b/src/main/java/pro/javacard/gp/GPRegistryEntryApp.java deleted file mode 100644 index 49548efc..00000000 --- a/src/main/java/pro/javacard/gp/GPRegistryEntryApp.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * GlobalPlatformPro - GlobalPlatform tool - * - * Copyright (C) 2015-2017 Martin Paljak, martin@martinpaljak.net - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3.0 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ -package pro.javacard.gp; - -import pro.javacard.AID; - -public final class GPRegistryEntryApp extends GPRegistryEntry { - private Privileges privileges; - private AID loadfile; - - public Privileges getPrivileges() { - return privileges; - } - - void setPrivileges(Privileges privs) { - privileges = privs; - } - - public AID getLoadFile() { - return loadfile; - } - - public void setLoadFile(AID aid) { - this.loadfile = aid; - } -} diff --git a/src/main/java/pro/javacard/gp/GPRegistryEntryPkg.java b/src/main/java/pro/javacard/gp/GPRegistryEntryPkg.java deleted file mode 100644 index 546086c6..00000000 --- a/src/main/java/pro/javacard/gp/GPRegistryEntryPkg.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * GlobalPlatformPro - GlobalPlatform tool - * - * Copyright (C) 2015-2017 Martin Paljak, martin@martinpaljak.net - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3.0 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ -package pro.javacard.gp; - -import apdu4j.HexUtils; -import pro.javacard.AID; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public final class GPRegistryEntryPkg extends GPRegistryEntry { - - private byte[] version; - private List modules = new ArrayList(); - - public byte[] getVersion() { - if (version == null) - return null; - return Arrays.copyOf(version, version.length); - } - - void setVersion(byte[] v) { - version = Arrays.copyOf(v, v.length); - } - - public String getVersionString() { - if (version == null) { - return ""; - } - if (version.length == 2) { - return version[0] + "." + version[1]; - } - return ""; - } - - public void addModule(AID aid) { - modules.add(aid); - } - - public List getModules() { - List r = new ArrayList(); - r.addAll(modules); - return r; - } -} diff --git a/src/main/java/pro/javacard/gp/PlaintextKeys.java b/src/main/java/pro/javacard/gp/PlaintextKeys.java deleted file mode 100644 index f7562778..00000000 --- a/src/main/java/pro/javacard/gp/PlaintextKeys.java +++ /dev/null @@ -1,306 +0,0 @@ -/* - * GlobalPlatformPro - GlobalPlatform tool - * - * Copyright (C) 2015-2017 Martin Paljak, martin@martinpaljak.net - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3.0 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ -package pro.javacard.gp; - -import apdu4j.HexUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import pro.javacard.gp.GPKey.Type; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -// Handles plaintext card keys. -// Supports diversification of card keys with some known algorithms. -public class PlaintextKeys extends GPSessionKeyProvider { - // Derivation constants - public static final Map SCP02_CONSTANTS; - public static final Map SCP03_CONSTANTS; - public static final Map SCP03_KDF_CONSTANTS; - private static final Logger logger = LoggerFactory.getLogger(PlaintextKeys.class); - - static { - HashMap scp2 = new HashMap<>(); - scp2.put(KeyPurpose.MAC, new byte[]{(byte) 0x01, (byte) 0x01}); - scp2.put(KeyPurpose.RMAC, new byte[]{(byte) 0x01, (byte) 0x02}); - scp2.put(KeyPurpose.DEK, new byte[]{(byte) 0x01, (byte) 0x81}); - scp2.put(KeyPurpose.ENC, new byte[]{(byte) 0x01, (byte) 0x82}); - SCP02_CONSTANTS = Collections.unmodifiableMap(scp2); - - HashMap scp3 = new HashMap<>(); - scp3.put(KeyPurpose.ENC, (byte) 0x04); - scp3.put(KeyPurpose.MAC, (byte) 0x06); - scp3.put(KeyPurpose.RMAC, (byte) 0x07); - SCP03_CONSTANTS = Collections.unmodifiableMap(scp3); - - HashMap scp3kdf = new HashMap<>(); - scp3kdf.put(KeyPurpose.ENC, HexUtils.hex2bin("0000000100")); - scp3kdf.put(KeyPurpose.MAC, HexUtils.hex2bin("0000000200")); - scp3kdf.put(KeyPurpose.DEK, HexUtils.hex2bin("0000000300")); - SCP03_KDF_CONSTANTS = Collections.unmodifiableMap(scp3kdf); - } - - // If diverisification is to be used, which method - Diversification diversifier = null; - - // Keyset version - private int version = 0; - private int id = 0; - // Holds card-specific keys. They shall be diversified in-place, as needed - private HashMap cardKeys = new HashMap<>(); - // Holds session-specific keys - private HashMap sessionKeys = new HashMap<>(); - - private PlaintextKeys() { - } - - public static PlaintextKeys fromMasterKey(GPKey master) { - return derivedFromMasterKey(master, null); - } - - public static PlaintextKeys derivedFromMasterKey(GPKey master, Diversification div) { - PlaintextKeys p = new PlaintextKeys(); - p.cardKeys.put(KeyPurpose.ENC, new GPKey(master.getBytes())); - p.cardKeys.put(KeyPurpose.MAC, new GPKey(master.getBytes())); - p.cardKeys.put(KeyPurpose.DEK, new GPKey(master.getBytes())); - p.diversifier = div; - return p; - } - - public static PlaintextKeys fromKeys(GPKey enc, GPKey mac, GPKey kek) { - PlaintextKeys p = new PlaintextKeys(); - p.cardKeys.put(KeyPurpose.ENC, enc); - p.cardKeys.put(KeyPurpose.MAC, mac); - p.cardKeys.put(KeyPurpose.DEK, kek); - return p; - } - - // Currently only support 3DES methods - // Purpose defines the magic constants for diversification - public static GPKey diversify(GPKey k, KeyPurpose usage, byte[] kdd, Diversification method) throws GPException { - try { - final byte[] kv; - - if (method == Diversification.KDF3) { - kv = GPCrypto.scp03_kdf(k.getBytes(), new byte[]{}, GPUtils.concatenate(SCP03_KDF_CONSTANTS.get(usage), kdd), k.getLength()); - return new GPKey(kv, Type.AES); - } else { - // shift around and fill initialize update data as required. - if (method == Diversification.VISA2) { - kv = fillVisa2(kdd, usage); - } else if (method == Diversification.EMV) { - kv = fillEmv(kdd, usage); - } else - throw new IllegalStateException("Unknown diversification method"); - - Cipher cipher = Cipher.getInstance(GPCrypto.DES3_ECB_CIPHER); - cipher.init(Cipher.ENCRYPT_MODE, k.getKeyAs(Type.DES3)); - // The resulting key can be interpreted as AES key (SCE 6.0) thus return as a RAW - // Caller can cast to whatever needed - return new GPKey(cipher.doFinal(kv)); - } - } catch (BadPaddingException | InvalidKeyException | IllegalBlockSizeException e) { - throw new GPException("Diversification failed.", e); - } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { - throw new RuntimeException("Can not diversify", e); - } - } - - public static final byte[] fillVisa2(byte[] kdd, KeyPurpose key) { - byte[] data = new byte[16]; - System.arraycopy(kdd, 0, data, 0, 2); - System.arraycopy(kdd, 4, data, 2, 4); - data[6] = (byte) 0xF0; - data[7] = key.getValue(); - System.arraycopy(kdd, 0, data, 8, 2); - System.arraycopy(kdd, 4, data, 10, 4); - data[14] = (byte) 0x0F; - data[15] = key.getValue(); - return data; - } - - // Unknown origin - public static final byte[] fillVisa(byte[] kdd, KeyPurpose key) { - byte[] data = new byte[16]; - System.arraycopy(kdd, 0, data, 0, 4); - System.arraycopy(kdd, 8, data, 4, 2); - data[6] = (byte) 0xF0; - data[7] = 0x01; - System.arraycopy(kdd, 0, data, 8, 4); - System.arraycopy(kdd, 8, data, 12, 2); - data[14] = (byte) 0x0F; - data[15] = 0x01; - return data; - } - - public static final byte[] fillEmv(byte[] kdd, KeyPurpose key) { - byte[] data = new byte[16]; - // 6 rightmost bytes of init update response (which is 10 bytes) - System.arraycopy(kdd, 4, data, 0, 6); - data[6] = (byte) 0xF0; - data[7] = key.getValue(); - System.arraycopy(kdd, 4, data, 8, 6); - data[14] = (byte) 0x0F; - data[15] = key.getValue(); - return data; - - } - - @Override - public int getVersion() { - return version; - } - - public void setVersion(int version) { - this.version = version; - } - - private GPKey deriveSessionKeySCP01(GPKey cardKey, KeyPurpose p, byte[] host_challenge, byte[] card_challenge) { - // RMAC is not supported - if (!(p == KeyPurpose.ENC || p == KeyPurpose.MAC || p == KeyPurpose.DEK)) { - throw new IllegalArgumentException("SCP 01 has only ENC, MAC, DEK: " + p); - } - - // DEK is not session based. - if (p == KeyPurpose.DEK) - return cardKey; - - byte[] derivationData = new byte[16]; - System.arraycopy(card_challenge, 4, derivationData, 0, 4); - System.arraycopy(host_challenge, 0, derivationData, 4, 4); - System.arraycopy(card_challenge, 0, derivationData, 8, 4); - System.arraycopy(host_challenge, 4, derivationData, 12, 4); - - try { - Cipher cipher = Cipher.getInstance(GPCrypto.DES3_ECB_CIPHER); - cipher.init(Cipher.ENCRYPT_MODE, cardKey.getKeyAs(Type.DES3)); - return new GPKey(cipher.doFinal(derivationData), Type.DES3); - } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { - throw new IllegalStateException("Can not calculate session keys", e); - } catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { - throw new RuntimeException("Session key calculation failed", e); - } - } - - private GPKey deriveSessionKeySCP02(GPKey cardKey, KeyPurpose p, byte[] sequence) { - if (p != KeyPurpose.ENC && p != KeyPurpose.MAC && p != KeyPurpose.DEK && p != KeyPurpose.RMAC) { - throw new IllegalArgumentException("SCP 02 has only ENC, MAC, DEK, RMAC: " + p); - } - // TODO: clarify RMAC/DEK - try { - Cipher cipher = Cipher.getInstance(GPCrypto.DES3_CBC_CIPHER); - byte[] derivationData = new byte[16]; - System.arraycopy(sequence, 0, derivationData, 2, 2); - System.arraycopy(SCP02_CONSTANTS.get(p), 0, derivationData, 0, 2); - cipher.init(Cipher.ENCRYPT_MODE, cardKey.getKeyAs(Type.DES3), GPCrypto.iv_null_8); - return new GPKey(cipher.doFinal(derivationData), Type.DES3); - } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { - throw new IllegalStateException("Session keys calculation failed.", e); - } catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) { - throw new RuntimeException("Session keys calculation failed.", e); - } - } - - private GPKey deriveSessionKeySCP03(GPKey cardKey, KeyPurpose p, byte[] host_challenge, byte[] card_challenge) { - if (p == KeyPurpose.DEK) { - return cardKey; - } - byte[] context = GPUtils.concatenate(host_challenge, card_challenge); - byte[] kdf = GPCrypto.scp03_kdf(cardKey, SCP03_CONSTANTS.get(p), context, cardKey.getLength() * 8); - return new GPKey(kdf, Type.AES); - } - - // Return true, if we can handle this card. - @Override - public boolean init(byte[] atr, byte[] cplc, byte[] kinfo) { - logger.debug("Card keys: {}", cardKeys.toString()); - return true; - } - - @Override - public void calculate(int scp, byte[] kdd, byte[] host_challenge, byte[] card_challenge, byte[] ssc) throws GPException { - // Check for arguments - if (scp == 1 || scp == 3) { - if (host_challenge == null || card_challenge == null) { - throw new IllegalArgumentException("SCP0" + scp + " requires host challenge and card challenge"); - } - } else if (scp == 2) { - if (ssc == null) { - throw new IllegalArgumentException("SCP02 requires sequence"); - } - } else { - throw new IllegalArgumentException("Don't know how to handle SCP0" + scp); - } - - logger.debug("Card keys: {}", cardKeys.toString()); - - // Calculate per-card keys from master key(s), if needed - if (diversifier != null) { - for (Map.Entry e : cardKeys.entrySet()) { - cardKeys.put(e.getKey(), diversify(e.getValue(), e.getKey(), kdd, diversifier)); - } - logger.trace("Derived per-card keys: {}", cardKeys.toString()); - } - - // Calculate session keys - for (Map.Entry e : cardKeys.entrySet()) { - if (scp == 1) { - sessionKeys.put(e.getKey(), deriveSessionKeySCP01(e.getValue(), e.getKey(), host_challenge, card_challenge)); - } else if (scp == 2) { - sessionKeys.put(e.getKey(), deriveSessionKeySCP02(e.getValue(), e.getKey(), ssc)); - } else if (scp == 3) { - sessionKeys.put(e.getKey(), deriveSessionKeySCP03(e.getValue(), e.getKey(), host_challenge, card_challenge)); - // Also make a RMAC key - if (e.getKey() == KeyPurpose.MAC) { - sessionKeys.put(KeyPurpose.RMAC, deriveSessionKeySCP03(e.getValue(), KeyPurpose.RMAC, host_challenge, card_challenge)); - } - } - } - logger.trace("Session keys: {}", sessionKeys.toString()); - } - - // Returns the key for the purpose for this session - @Override - public GPKey getKeyFor(KeyPurpose p) { - return sessionKeys.get(p); - } - - @Override - public int getID() { - return id; - } - - public void setDiversifier(Diversification diversifier) { - this.diversifier = diversifier; - } - - // diversification methods - public enum Diversification { - VISA2, EMV, KDF3 - } -} diff --git a/tool/pom.xml b/tool/pom.xml new file mode 100644 index 00000000..c2b64aae --- /dev/null +++ b/tool/pom.xml @@ -0,0 +1,104 @@ + + + 4.0.0 + + + com.github.martinpaljak + gppro + 19.05.16 + + + gptool + + + + + com.github.martinpaljak + apdu4j-pcsc + 19.05.08 + + + + com.github.martinpaljak + globalplatformpro + 19.05.16 + + + + com.google.code.gson + gson + 2.8.5 + + + + org.apache.httpcomponents + httpclient + 4.5.8 + + + + org.slf4j + slf4j-simple + 1.7.25 + true + + + + org.slf4j + slf4j-api + 1.7.25 + + + + net.sf.jopt-simple + jopt-simple + 5.0.4 + + + + org.testng + testng + 6.14.3 + test + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + package + + shade + + + gp + + + pro.javacard.gp.GPTool + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + META-INF/maven/** + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/pro/javacard/gp/DAPProperties.java b/tool/src/main/java/pro/javacard/gp/DAPProperties.java similarity index 50% rename from src/main/java/pro/javacard/gp/DAPProperties.java rename to tool/src/main/java/pro/javacard/gp/DAPProperties.java index 717727a2..8f7e1531 100644 --- a/src/main/java/pro/javacard/gp/DAPProperties.java +++ b/tool/src/main/java/pro/javacard/gp/DAPProperties.java @@ -2,8 +2,9 @@ import joptsimple.OptionSet; import pro.javacard.AID; +import pro.javacard.gp.GPRegistryEntry.Privilege; -import javax.smartcardio.CardException; +import java.io.IOException; import static pro.javacard.gp.GPCommandLineInterface.OPT_DAP_DOMAIN; import static pro.javacard.gp.GPCommandLineInterface.OPT_TO; @@ -13,29 +14,26 @@ public class DAPProperties { private AID dapDomain = null; private boolean required = false; - public DAPProperties(OptionSet args, GlobalPlatform gp) throws CardException, GPException { + public DAPProperties(OptionSet args, GPSession gp) throws IOException, IllegalArgumentException { + GPRegistry reg = gp.getRegistry(); // Override target and check for DAP if (args.has(OPT_TO)) { targetDomain = AID.fromString(args.valueOf(OPT_TO)); - if (gp.getRegistry().getDomain(targetDomain) == null) { - throw new GPException("Specified target domain is invalid: " + targetDomain); - } - if (gp.getRegistry().getDomain(targetDomain).getPrivileges().has(GPRegistryEntry.Privilege.DAPVerification)) - required = true; + GPRegistryEntry target = reg.getDomain(targetDomain).orElseThrow(() -> new IllegalArgumentException("Target domain does not exist: " + targetDomain)); + + required = required || target.hasPrivilege(Privilege.DAPVerification); } // Check if DAP block is required - for (GPRegistryEntryApp e : gp.getRegistry().allDomains()) { - if (e.getPrivileges().has(GPRegistryEntry.Privilege.MandatedDAPVerification)) - required = true; - } + required = required || reg.allDomains().stream().anyMatch( e->e.hasPrivilege(Privilege.MandatedDAPVerification)); // Check if DAP is overriden if (args.has(OPT_DAP_DOMAIN)) { dapDomain = AID.fromString(args.valueOf(OPT_DAP_DOMAIN)); - GPRegistryEntry.Privileges p = gp.getRegistry().getDomain(dapDomain).getPrivileges(); - if (!(p.has(GPRegistryEntry.Privilege.DAPVerification) || p.has(GPRegistryEntry.Privilege.MandatedDAPVerification))) { - throw new GPException("Specified DAP domain does not have (Mandated)DAPVerification privilege: " + p.toString()); + GPRegistryEntry target = reg.getDomain(targetDomain).orElseThrow(() -> new IllegalArgumentException("DAP domain does not exist: " + targetDomain)); + + if (!(target.hasPrivilege(Privilege.DAPVerification) || target.hasPrivilege(Privilege.MandatedDAPVerification))) { + throw new GPException("Specified DAP domain does not have (Mandated)DAPVerification privilege: " + targetDomain.toString()); } } } diff --git a/src/main/java/pro/javacard/gp/GPCommandLineInterface.java b/tool/src/main/java/pro/javacard/gp/GPCommandLineInterface.java similarity index 97% rename from src/main/java/pro/javacard/gp/GPCommandLineInterface.java rename to tool/src/main/java/pro/javacard/gp/GPCommandLineInterface.java index cf970ad0..347c4d6e 100644 --- a/src/main/java/pro/javacard/gp/GPCommandLineInterface.java +++ b/tool/src/main/java/pro/javacard/gp/GPCommandLineInterface.java @@ -83,8 +83,8 @@ abstract class GPCommandLineInterface { protected final static String OPT_SET_PRE_PERSO = "set-pre-perso"; protected final static String OPT_SET_PERSO = "set-perso"; protected final static String OPT_SHA256 = "sha256"; - protected final static String OPT_STORE_DATA_BLOB = "store-data"; - protected final static String OPT_STORE_DATA = "store-data-chunk"; + protected final static String OPT_STORE_DATA = "store-data"; + protected final static String OPT_STORE_DATA_CHUNK = "store-data-chunk"; protected final static String OPT_TERMINALS = "terminals"; protected final static String OPT_TERMINATE = "terminate"; protected final static String OPT_TODAY = "today"; @@ -159,8 +159,8 @@ protected static OptionSet parseArguments(String[] argv) throws IOException { parser.accepts(OPT_SET_PERSO, "Set Perso data in CPLC").withRequiredArg().describedAs("data"); parser.accepts(OPT_TODAY, "Set date to today when updating CPLC"); - parser.accepts(OPT_STORE_DATA_BLOB, "STORE DATA blob").withRequiredArg().describedAs("data"); - parser.accepts(OPT_STORE_DATA, "Send STORE DATA commands").withRequiredArg().describedAs("data"); + parser.accepts(OPT_STORE_DATA, "STORE DATA blob").withRequiredArg().describedAs("data"); + parser.accepts(OPT_STORE_DATA_CHUNK, "Send STORE DATA commands").withRequiredArg().describedAs("data"); parser.accepts(OPT_TOKEN_KEY, "Path to private key used in Delegated Management token generation").withRequiredArg().describedAs("path"); diff --git a/src/main/java/pro/javacard/gp/GPTool.java b/tool/src/main/java/pro/javacard/gp/GPTool.java similarity index 87% rename from src/main/java/pro/javacard/gp/GPTool.java rename to tool/src/main/java/pro/javacard/gp/GPTool.java index eeae15c9..a10dc640 100644 --- a/src/main/java/pro/javacard/gp/GPTool.java +++ b/tool/src/main/java/pro/javacard/gp/GPTool.java @@ -20,15 +20,17 @@ */ package pro.javacard.gp; +import apdu4j.CommandAPDU; import apdu4j.*; +import apdu4j.providers.APDUReplayProvider; +import apdu4j.terminals.LoggingCardTerminal; import joptsimple.OptionSet; import pro.javacard.AID; import pro.javacard.CAPFile; -import pro.javacard.gp.GPKey.Type; import pro.javacard.gp.GPRegistryEntry.Privilege; import pro.javacard.gp.GPRegistryEntry.Privileges; -import pro.javacard.gp.GlobalPlatform.APDUMode; -import pro.javacard.gp.GlobalPlatform.GPSpec; +import pro.javacard.gp.GPSession.APDUMode; +import pro.javacard.gp.GPSession.GPSpec; import javax.crypto.Cipher; import javax.smartcardio.*; @@ -41,8 +43,6 @@ import java.util.*; import java.util.stream.Collectors; -import static pro.javacard.gp.PlaintextKeys.Diversification.*; - public final class GPTool extends GPCommandLineInterface { private static boolean isVerbose = false; @@ -67,7 +67,7 @@ public static void main(String[] argv) throws Exception { } if (args.has(OPT_VERSION) || args.has(OPT_VERBOSE) || args.has(OPT_DEBUG) || args.has(OPT_INFO)) { - String version = GlobalPlatform.getVersion(); + String version = GPSession.getVersion(); // Append host information version += "\nRunning on " + System.getProperty("os.name"); version += " " + System.getProperty("os.version"); @@ -182,7 +182,7 @@ public static void main(String[] argv) throws Exception { } Card card = null; - CardChannel channel = null; + APDUBIBO channel = null; try { // Establish connection try { @@ -190,7 +190,7 @@ public static void main(String[] argv) throws Exception { // We use apdu4j which by default uses jnasmartcardio // which uses real SCardBeginTransaction card.beginExclusive(); - channel = card.getBasicChannel(); + channel = CardChannelBIBO.getBIBO(card.getBasicChannel()); } catch (CardException e) { System.err.println("Could not connect to " + reader.getName() + ": " + TerminalManager.getExceptionMessage(e)); continue; @@ -226,14 +226,14 @@ public static void main(String[] argv) throws Exception { Map env = System.getenv(); // GlobalPlatform specific - final GlobalPlatform gp; + final GPSession gp; if (args.has(OPT_SDAID)) { - gp = GlobalPlatform.connect(channel, AID.fromString(args.valueOf(OPT_SDAID))); + gp = GPSession.connect(channel, AID.fromString(args.valueOf(OPT_SDAID))); } else if (env.containsKey("GP_AID")) { - gp = GlobalPlatform.connect(channel, AID.fromString(env.get("GP_AID"))); + gp = GPSession.connect(channel, AID.fromString(env.get("GP_AID"))); } else { // Oracle only applies if no other arguments given - gp = GlobalPlatform.discover(channel); + gp = GPSession.discover(channel); // FIXME: would like to get AID from oracle as well. } @@ -253,55 +253,50 @@ public static void main(String[] argv) throws Exception { } // Normally assume a single master key - final GPSessionKeyProvider keys; + final GPCardKeys keys; if (args.has(OPT_KEYS)) { // keys come from custom provider fail("Not yet implemented"); - keys = PlaintextKeys.fromMasterKey(GPData.getDefaultKey()); + keys = PlaintextKeys.defaultKey(); } else if (args.has(OPT_ORACLE)) { keys = PythiaKeys.ask(card.getATR().getBytes(), GPData.fetchCPLC(channel), GPData.fetchKeyInfoTemplate(channel)); } else { PlaintextKeys keyz; if (args.has(OPT_KEY)) { - GPKey k = new GPKey(HexUtils.stringToBin((String) args.valueOf(OPT_KEY))); + byte[] k = HexUtils.stringToBin((String) args.valueOf(OPT_KEY)); + byte[] kcv = null; + if (args.has(OPT_KCV)) { - byte[] given = HexUtils.stringToBin((String) args.valueOf(OPT_KCV)); - byte[] expected = k.getKCV(); - if (expected.length == 0) { - fail("Don't know how to calculate KCV for the key"); // FIXME: all keys are RAW currently - } - // Check KCV - if (!Arrays.equals(given, expected)) { - fail("KCV does not match, expected " + HexUtils.bin2hex(expected) + " but given " + HexUtils.bin2hex(given)); - } + kcv = HexUtils.stringToBin((String) args.valueOf(OPT_KCV)); } - keyz = PlaintextKeys.fromMasterKey(k); + + keyz = PlaintextKeys.fromMasterKey(k, kcv); } else { Optional params = SecureChannelParameters.fromEnvironment(); // XXX: better checks for exclusive key options if (args.has(OPT_KEY_MAC) && args.has(OPT_KEY_ENC) && args.has(OPT_KEY_DEK)) { - GPKey enc = new GPKey(HexUtils.stringToBin((String) args.valueOf(OPT_KEY_ENC))); - GPKey mac = new GPKey(HexUtils.stringToBin((String) args.valueOf(OPT_KEY_MAC))); - GPKey dek = new GPKey(HexUtils.stringToBin((String) args.valueOf(OPT_KEY_DEK))); + byte[] enc = HexUtils.stringToBin((String) args.valueOf(OPT_KEY_ENC)); + byte[] mac = HexUtils.stringToBin((String) args.valueOf(OPT_KEY_MAC)); + byte[] dek = HexUtils.stringToBin((String) args.valueOf(OPT_KEY_DEK)); keyz = PlaintextKeys.fromKeys(enc, mac, dek); } else if (params.isPresent()) { - keyz = (PlaintextKeys) params.get().getSessionKeys(); + keyz = (PlaintextKeys) params.get().getCardKeys(); } else { if (needsAuthentication(args)) { - System.out.println("Warning: no keys given, using default test key " + HexUtils.bin2hex(GPData.defaultKeyBytes)); + System.out.println("Warning: no keys given, using default test key " + HexUtils.bin2hex(PlaintextKeys.defaultKeyBytes)); } - keyz = PlaintextKeys.fromMasterKey(GPData.getDefaultKey()); + keyz = PlaintextKeys.defaultKey(); } } // "gp -l -emv" should still work if (args.has(OPT_VISA2)) { - keyz.setDiversifier(VISA2); + keyz.setDiversifier(PlaintextKeys.Diversification.VISA2); } else if (args.has(OPT_EMV)) { - keyz.setDiversifier(EMV); + keyz.setDiversifier(PlaintextKeys.Diversification.EMV); } else if (args.has(OPT_KDF3)) { - keyz.setDiversifier(KDF3); + keyz.setDiversifier(PlaintextKeys.Diversification.KDF3); } if (args.has(OPT_KEY_VERSION)) { @@ -329,7 +324,7 @@ public static void main(String[] argv) throws Exception { // Authenticate, only if needed if (needsAuthentication(args)) { - EnumSet mode = GlobalPlatform.defaultMode.clone(); + EnumSet mode = GPSession.defaultMode.clone(); // Override default mode if needed. if (args.has(OPT_SC_MODE)) { mode.clear(); @@ -362,9 +357,11 @@ public static void main(String[] argv) throws Exception { GPRegistry reg = gp.getRegistry(); // DWIM: assume that default selected is the one to be deleted - if (args.has(OPT_DEFAULT) && reg.getDefaultSelectedAID() != null) { - if (reg.getDefaultSelectedPackageAID() != null) { - gp.deleteAID(reg.getDefaultSelectedPackageAID(), true); + if (args.has(OPT_DEFAULT)) { + Optional def = reg.getDefaultSelectedAID(); + + if (def.isPresent()) { + gp.deleteAID(def.get(), false); } else { System.err.println("Could not identify default selected application!"); } @@ -485,8 +482,8 @@ public static void main(String[] argv) throws Exception { Privileges privs = getInstPrivs(args); // Remove existing default app - if (args.has(OPT_FORCE) && (reg.getDefaultSelectedAID() != null && privs.has(Privilege.CardReset))) { - gp.deleteAID(reg.getDefaultSelectedAID(), false); + if (args.has(OPT_FORCE) && (reg.getDefaultSelectedAID().isPresent() && privs.has(Privilege.CardReset))) { + gp.deleteAID(reg.getDefaultSelectedAID().get(), false); } // warn @@ -587,8 +584,8 @@ public static void main(String[] argv) throws Exception { // --store-data // This will split the data, if necessary - if (args.has(OPT_STORE_DATA_BLOB)) { - List blobs = args.valuesOf(OPT_STORE_DATA_BLOB).stream().map(e -> HexUtils.stringToBin((String) e)).collect(Collectors.toList()); + if (args.has(OPT_STORE_DATA)) { + List blobs = args.valuesOf(OPT_STORE_DATA).stream().map(e -> HexUtils.stringToBin((String) e)).collect(Collectors.toList()); for (byte[] blob : blobs) { if (args.has(OPT_APPLET)) { gp.personalize(AID.fromString(args.valueOf(OPT_APPLET)), blob, 0x01); @@ -600,8 +597,8 @@ public static void main(String[] argv) throws Exception { // --store-data-chunk // This will collect the chunks and send them one by one - if (args.has(OPT_STORE_DATA)) { - List blobs = args.valuesOf(OPT_STORE_DATA).stream().map(e -> HexUtils.stringToBin((String) e)).collect(Collectors.toList()); + if (args.has(OPT_STORE_DATA_CHUNK)) { + List blobs = args.valuesOf(OPT_STORE_DATA_CHUNK).stream().map(e -> HexUtils.stringToBin((String) e)).collect(Collectors.toList()); if (args.has(OPT_APPLET)) { gp.personalize(AID.fromString(args.valueOf(OPT_APPLET)), blobs, 0x01); } else { @@ -664,11 +661,7 @@ public static void main(String[] argv) throws Exception { // --secure-card if (args.has(OPT_SECURE_CARD)) { // Skip INITIALIZED - GPRegistryEntryApp isd = gp.getRegistry().getISD(); - if (isd == null) { - GPCommands.listRegistry(gp.getRegistry(), System.out, true); - fail("ISD is null"); - } + GPRegistryEntry isd = gp.getRegistry().getISD().orElseThrow(() -> new GPException("ISD is null")); if (isd.getLifeCycle() != GPData.initializedStatus) { if (args.has(OPT_FORCE)) { System.out.println("Note: forcing status to INITIALIZED"); @@ -705,7 +698,6 @@ public static void main(String[] argv) throws Exception { // --unlock if (args.has(OPT_UNLOCK)) { // Write default keys - List newkeys = new ArrayList<>(); final boolean replace; final int kv; // Factory keys @@ -718,24 +710,17 @@ public static void main(String[] argv) throws Exception { replace = true; } - // FIXME: new key must adhere to currently used SCP version. - GPKey new_key = new GPKey(GPData.defaultKeyBytes, gp.getSCPVersion() == 3 ? Type.AES : Type.DES3); - - // XXX: ID handling ? - newkeys.add(new GPKey(kv, 1, new_key)); - newkeys.add(new GPKey(kv, 2, new_key)); - newkeys.add(new GPKey(kv, 3, new_key)); - - gp.putKeys(newkeys, replace); - - System.out.println("Default " + new_key.toString() + " set as master key for " + gp.getAID()); + PlaintextKeys new_key = PlaintextKeys.defaultKey(); + new_key.setVersion(kv); + gp.putKeys(new_key, replace); + System.out.println("Default " + HexUtils.bin2hex(PlaintextKeys.defaultKeyBytes) + " set as master key for " + gp.getAID()); } // --lock if (args.has(OPT_LOCK) || (args.has(OPT_LOCK_ENC) && args.has(OPT_LOCK_MAC) && args.has(OPT_LOCK_DEK))) { // By default we try to change an existing key boolean replace = true; - List current = gp.getKeyInfoTemplate(); + List current = gp.getKeyInfoTemplate(); // By default use key version 1 int new_version = 1; @@ -750,34 +735,27 @@ public static void main(String[] argv) throws Exception { } } - // If a specific new key version is specified, use that instead. - if (args.has(OPT_NEW_KEY_VERSION)) { - new_version = GPUtils.intValue((String) args.valueOf(OPT_NEW_KEY_VERSION)); - replace = false; - System.out.println("New version: " + new_version); - } - // Get key value or values - List updatekeys = new ArrayList<>(); + PlaintextKeys newKeys; if (args.has(OPT_LOCK_ENC) && args.has(OPT_LOCK_MAC) && args.has(OPT_LOCK_DEK)) { - updatekeys.add(new GPKey(new_version, 1, new GPKey(HexUtils.stringToBin((String) args.valueOf(OPT_LOCK_ENC))))); - updatekeys.add(new GPKey(new_version, 2, new GPKey(HexUtils.stringToBin((String) args.valueOf(OPT_LOCK_MAC))))); - updatekeys.add(new GPKey(new_version, 3, new GPKey(HexUtils.stringToBin((String) args.valueOf(OPT_LOCK_DEK))))); + byte[] enc = HexUtils.stringToBin((String) args.valueOf(OPT_LOCK_ENC)); + byte[] mac = HexUtils.stringToBin((String) args.valueOf(OPT_LOCK_MAC)); + byte[] dek = HexUtils.stringToBin((String) args.valueOf(OPT_LOCK_DEK)); + newKeys = PlaintextKeys.fromKeys(enc, mac, dek); } else { - GPKey nk = new GPKey(HexUtils.stringToBin((String) args.valueOf(OPT_LOCK))); - // We currently use the same key, diversification is missing - updatekeys.add(new GPKey(new_version, 1, nk)); - updatekeys.add(new GPKey(new_version, 2, nk)); - updatekeys.add(new GPKey(new_version, 3, nk)); + newKeys = PlaintextKeys.fromMasterKey(HexUtils.stringToBin((String) args.valueOf(OPT_LOCK))); } - // XXX: this is uggely - Type t = gp.getSCPVersion() == 3 ? Type.AES : Type.DES3; - for (GPKey k : updatekeys) { - k.become(t); + // If a specific new key version is specified, use that instead. + if (args.has(OPT_NEW_KEY_VERSION)) { + new_version = GPUtils.intValue((String) args.valueOf(OPT_NEW_KEY_VERSION)); + replace = false; + System.out.println("New version: " + new_version); } + newKeys.setVersion(new_version); + newKeys.diversify(gp.scpVersion, null); // FIXME: put scp02 keys with scp03 - gp.putKeys(updatekeys, replace); + gp.putKeys(newKeys, replace); if (args.has(OPT_LOCK)) { System.out.println("Card locked with: " + HexUtils.bin2hex(HexUtils.stringToBin((String) args.valueOf(OPT_LOCK)))); @@ -815,14 +793,11 @@ public static void main(String[] argv) throws Exception { } } } catch (GPException e) { - //if (args.has(OPT_DEBUG)) { - // e.printStackTrace(System.err); - // } // All unhandled GP exceptions halt the program unless it is run with -force if (!args.has(OPT_FORCE)) { fail(e.getMessage()); } - } catch (CardException e) { + } catch (IOException e) { System.out.println("Failed to communicate with card in " + reader + ": " + e.getMessage()); // Card exceptions skip to the next reader, if available and allowed FIXME broken logic continue; @@ -830,7 +805,6 @@ public static void main(String[] argv) throws Exception { if (card != null) { card.endExclusive(); card.disconnect(true); - card = null; } } } @@ -847,7 +821,7 @@ public static void main(String[] argv) throws Exception { System.exit(0); } - private static void calculateDapPropertiesAndLoadCap(OptionSet args, GlobalPlatform gp, CAPFile capFile) throws GPException, CardException { + private static void calculateDapPropertiesAndLoadCap(OptionSet args, GPSession gp, CAPFile capFile) throws GPException, IOException { try { DAPProperties dap = new DAPProperties(args, gp); loadCapAccordingToDapRequirement(args, gp, dap.getTargetDomain(), dap.getDapDomain(), dap.isRequired(), capFile); @@ -867,7 +841,7 @@ private static void calculateDapPropertiesAndLoadCap(OptionSet args, GlobalPlatf } } - private static void loadCapAccordingToDapRequirement(OptionSet args, GlobalPlatform gp, AID targetDomain, AID dapDomain, boolean dapRequired, CAPFile cap) throws CardException, GPException { + private static void loadCapAccordingToDapRequirement(OptionSet args, GPSession gp, AID targetDomain, AID dapDomain, boolean dapRequired, CAPFile cap) throws IOException, GPException { // XXX: figure out right signature type in a better way if (dapRequired) { byte[] dap = args.has(OPT_SHA256) ? cap.getMetaInfEntry(CAPFile.DAP_RSA_V1_SHA256_FILE) : cap.getMetaInfEntry(CAPFile.DAP_RSA_V1_SHA1_FILE); @@ -877,7 +851,8 @@ private static void loadCapAccordingToDapRequirement(OptionSet args, GlobalPlatf } } - private static PrivateKey getRSAPrivateKey(String path) { + @Deprecated + private static PrivateKey getRSAPrivateKey(String path) throws IOException { try (FileInputStream fin = new FileInputStream(new File(path))) { PrivateKey key = GPCrypto.pem2PrivateKey(fin); if (key instanceof RSAPrivateKey) { @@ -885,8 +860,6 @@ private static PrivateKey getRSAPrivateKey(String path) { } else { throw new RuntimeException("Supplied key at path is not instance of RSAPrivateKey"); } - } catch (Exception e) { - throw new RuntimeException("Could not extract RSAPrivateKey from supplied path"); } } @@ -930,6 +903,7 @@ private static byte[] getInstParams(OptionSet args) { } } + // TODO: apdu4j has this covered private static boolean ignoreReader(String name) { String ignore = System.getenv("GP_READER_IGNORE"); if (ignore != null) { @@ -958,15 +932,10 @@ private static boolean needsAuthentication(OptionSet args) { String[] yes = new String[]{OPT_LIST, OPT_LOAD, OPT_INSTALL, OPT_DELETE, OPT_DELETE_KEY, OPT_CREATE, OPT_ACR_ADD, OPT_ACR_DELETE, OPT_LOCK, OPT_UNLOCK, OPT_LOCK_ENC, OPT_LOCK_MAC, OPT_LOCK_DEK, OPT_MAKE_DEFAULT, OPT_UNINSTALL, OPT_SECURE_APDU, OPT_DOMAIN, OPT_LOCK_CARD, OPT_UNLOCK_CARD, OPT_LOCK_APPLET, OPT_UNLOCK_APPLET, - OPT_STORE_DATA_BLOB, OPT_STORE_DATA, OPT_INITIALIZE_CARD, OPT_SECURE_CARD, OPT_RENAME_ISD, OPT_SET_PERSO, OPT_SET_PRE_PERSO, OPT_MOVE, + OPT_STORE_DATA, OPT_STORE_DATA_CHUNK, OPT_INITIALIZE_CARD, OPT_SECURE_CARD, OPT_RENAME_ISD, OPT_SET_PERSO, OPT_SET_PRE_PERSO, OPT_MOVE, OPT_PUT_KEY, OPT_ACR_AID, OPT_ACR_LIST}; - for (String s : yes) { - if (args.has(s)) { - return true; - } - } - return false; + return Arrays.stream(yes).anyMatch(str -> args.has(str)); } public static void fail(String msg) {