add missing Delete command code

This commit is contained in:
Andrea Franz 2018-09-10 15:56:47 +02:00
parent 75ec4d0723
commit 61dbec1a7f
No known key found for this signature in database
GPG Key ID: 4F0D2F2D9DE7F29D
19 changed files with 1569 additions and 0 deletions

View File

@ -0,0 +1,25 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
public class Delete {
private static final int CLA = 0x80;
private static final int INS = 0xE4;
private static final int P1 = 0x00;
private static final int P2 = 0x80; // delete object and related files
private byte[] aid;
public Delete(byte[] aid) {
this.aid = aid;
}
public APDUCommand getCommand() {
byte[] data = new byte[this.aid.length + 2];
data[0] = 0x4F;
data[1] = (byte) this.aid.length;
System.arraycopy(this.aid, 0, data, 2, this.aid.length);
return new APDUCommand(CLA, INS, P1, P2, data);
}
}

View File

@ -0,0 +1,39 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.APDUResponse;
import im.status.applet_installer_test.appletinstaller.Crypto;
public class ExternalAuthenticate {
public static int CLA = 0x84;
public static int INS = 0x82;
public static int P1 = 0x01;
public static int P2 = 0x00;
private byte[] encKeyData;
private byte[] cardChallenge;
private byte[] hostChallenge;
public ExternalAuthenticate(byte[] encKeyData, byte[] cardChallenge, byte[] hostChallenge) {
this.encKeyData = encKeyData;
this.cardChallenge = cardChallenge;
this.hostChallenge = hostChallenge;
}
public APDUCommand getCommand() {
return new APDUCommand(CLA, INS, P1, P2, this.getHostCryptogram());
}
public byte[] getHostCryptogram() {
byte[] data = new byte[this.cardChallenge.length + this.hostChallenge.length];
System.arraycopy(cardChallenge, 0, data, 0, cardChallenge.length);
System.arraycopy(hostChallenge, 0, data, cardChallenge.length, hostChallenge.length);
byte[] paddedData = Crypto.appendDESPadding(data);
return Crypto.mac3des(this.encKeyData, paddedData, Crypto.NullBytes8);
}
public boolean checkResponse(APDUResponse resp) {
return resp.getSw() == APDUResponse.SW_OK;
}
}

View File

@ -0,0 +1,8 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
public class GetData {
public static final int CLA = 0x80;
public static final int INS = 0x50;
public static final int P1 = 0;
public static final int P2 = 0;
}

View File

@ -0,0 +1,78 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import java.security.SecureRandom;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.APDUException;
import im.status.applet_installer_test.appletinstaller.APDUResponse;
import im.status.applet_installer_test.appletinstaller.Crypto;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import im.status.applet_installer_test.appletinstaller.Keys;
import im.status.applet_installer_test.appletinstaller.Logger;
import im.status.applet_installer_test.appletinstaller.Session;
public class InitializeUpdate {
public static final int CLA = 0x80;
public static final int INS = 0x50;
public static final int P1 = 0;
public static final int P2 = 0;
public static byte[] DERIVATION_PURPOSE_ENC = new byte[]{(byte) 0x01, (byte) 0x82};
public static byte[] DERIVATION_PURPOSE_MAC = new byte[]{(byte) 0x01, (byte) 0x01};
public static byte[] DERIVATION_PURPOSE_DEK = new byte[]{(byte) 0x01, (byte) 0x81};
private byte[] hostChallenge;
public InitializeUpdate(byte[] challenge) {
this.hostChallenge = challenge;
}
public APDUCommand getCommand() {
return new APDUCommand(CLA, INS, P1, P2, this.hostChallenge, true);
}
public static byte[] generateChallenge() {
SecureRandom random = new SecureRandom();
byte challenge[] = new byte[8];
random.nextBytes(challenge);
return challenge;
}
public Session verifyResponse(Keys cardKeys, APDUResponse resp) throws APDUException {
if (resp.getSw() == APDUResponse.SW_SECURITY_CONDITION_NOT_SATISFIED) {
throw new APDUException(resp.getSw(), "security confition not satisfied");
}
if (resp.getSw() == APDUResponse.SW_AUTHENTICATION_METHOD_BLOCKED) {
throw new APDUException(resp.getSw(), "authentication method blocked");
}
byte[] data = resp.getData();
if (data.length != 28) {
throw new APDUException(resp.getSw(), String.format("bad data length, expected 28, got %d", data.length));
}
byte[] cardChallenge = new byte[8];
System.arraycopy(data, 12, cardChallenge, 0, 8);
byte[] cardCryptogram = new byte[8];
System.arraycopy(data, 20, cardCryptogram, 0, 8);
byte[] seq = new byte[2];
System.arraycopy(data, 12, seq, 0, 2);
byte[] sessionEncKey = Crypto.deriveKey(cardKeys.getEncKeyData(), seq, DERIVATION_PURPOSE_ENC);
byte[] sessionMacKey = Crypto.deriveKey(cardKeys.getMacKeyData(), seq, DERIVATION_PURPOSE_MAC);
Keys sessionKeys = new Keys(sessionEncKey, sessionMacKey);
boolean verified = Crypto.verifyCryptogram(sessionKeys.getEncKeyData(), this.hostChallenge, cardChallenge, cardCryptogram);
if (!verified) {
throw new APDUException("error verifying card cryptogram.");
}
return new Session(sessionKeys, cardChallenge);
}
}

View File

@ -0,0 +1,52 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
public class InstallForInstall {
public static final int CLA = 0x80;
public static final int INS = 0xE6;
public static final int P1 = 0x0C;
public static final int P2 = 0;
private byte[] packageAID;
private byte[] appletAID;
private byte[] instanceAID;
private byte[] params;
public InstallForInstall(byte[] packageAID, byte[] appletAID, byte[] instanceAID, byte[] params) {
this.packageAID = packageAID;
this.appletAID = appletAID;
this.instanceAID = instanceAID;
this.params = params;
}
public APDUCommand getCommand() throws IOException {
ByteArrayOutputStream data = new ByteArrayOutputStream();
data.write(this.packageAID.length);
data.write(this.packageAID);
data.write(this.appletAID.length);
data.write(this.appletAID);
data.write(this.instanceAID.length);
data.write(this.instanceAID);
byte[] privileges = new byte[]{0x00};
data.write(privileges.length);
data.write(privileges);
byte[] fullParams = new byte[2 + params.length];
fullParams[0] = (byte) 0xC9;
fullParams[1] = (byte) params.length;
System.arraycopy(params, 0, fullParams, 2, params.length);
data.write(fullParams.length);
data.write(fullParams);
// empty install token
data.write(0x00);
return new APDUCommand(CLA, INS, P1, P2, data.toByteArray() );
}
}

View File

@ -0,0 +1,36 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
public class InstallForLoad {
public static final int CLA = 0x80;
public static final int INS = 0xE6;
public static final int P1 = 0x02;
public static final int P2 = 0;
private byte[] aid;
private byte[] sdaid;
public InstallForLoad(byte[] aid, byte[] sdaid) {
this.aid = aid;
this.sdaid = sdaid;
}
public APDUCommand getCommand() throws IOException {
ByteArrayOutputStream data = new ByteArrayOutputStream();
data.write(this.aid.length);
data.write(this.aid);
data.write(this.sdaid.length);
data.write(this.sdaid);
// empty hash length and hash
data.write(0x00);
data.write(0x00);
data.write(0x00);
return new APDUCommand(CLA, INS, P1, P2, data.toByteArray());
}
}

View File

@ -0,0 +1,135 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.HexUtils;
public class Load {
public static final int CLA = 0x80;
public static final int INS = 0xE8;
private static String[] fileNames = {"Header", "Directory", "Import", "Applet",
"Class", "Method", "StaticField", "Export", "ConstantPool", "RefLocation"};
private String path;
private int offset;
private int count;
private byte[] fullData;
public Load(InputStream in) throws FileNotFoundException, IOException {
this.path = path;
this.offset = 0;
this.count = 0;
Map<String, byte[]> files = this.loadFiles(in);
in.close();
this.fullData = this.getCode(files);
}
public Map<String, byte[]> loadFiles(InputStream in) throws IOException {
Map<String, byte[]> files = new LinkedHashMap<>();
ZipInputStream zip = new ZipInputStream(in);
ZipEntry entry = zip.getNextEntry();
while(entry != null) {
ByteArrayOutputStream data = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int count;
while ((count = zip.read(buf)) != -1) {
data.write(buf, 0, count);
}
String name = baseName(entry.getName());
files.put(name, data.toByteArray());
entry = zip.getNextEntry();
}
return files;
}
private String baseName(String path) {
String[] parts = path.split("[/.]");
return parts[parts.length - 2];
}
public APDUCommand getCommand() {
int blockSize = 247; // 255 - 8 bytes for MAC
if (this.offset >= this.fullData.length) {
return null;
}
int rangeEnd = this.offset + blockSize;
if (rangeEnd >= this.fullData.length) {
rangeEnd = this.fullData.length;
}
int size = rangeEnd - offset;
byte[] data = new byte[size];
System.arraycopy(this.fullData, this.offset, data, 0, size);
boolean isLast = this.offset + size >= this.fullData.length;
int p1 = isLast ? 0x80 : 0;
APDUCommand cmd = new APDUCommand(CLA, INS, p1, this.count, data);
this.offset += size;
this.count++;
return cmd;
}
private byte[] encodeFullLength(int length) {
if (length < 0x80) {
return new byte[]{(byte) length};
} else if (length < 0xFF) {
return new byte[]{(byte) 0x81, (byte) length};
} else if (length < 0xFFFF) {
return new byte[]{
(byte) 0x82,
(byte) ((length & 0xFF00) >> 8),
(byte) (length & 0xFF),
};
} else {
return new byte[]{
(byte) 0x83,
(byte) ((length & 0xFF0000) >> 16),
(byte) ((length & 0xFF00) >> 8),
(byte) (length & 0xFF),
};
}
}
public byte[] getCode(Map<String, byte[]> files) throws IOException {
ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
for (String name : fileNames) {
byte[] fileData = files.get(name);
if (fileData == null) {
continue;
}
dataStream.write(fileData);
}
byte[] data = dataStream.toByteArray();
byte[] encodedFullLength = encodeFullLength(data.length);
byte[] fullData = new byte[1 + encodedFullLength.length + data.length];
fullData[0] = (byte) 0xC4;
System.arraycopy(encodedFullLength, 0, fullData, 1, encodedFullLength.length);
System.arraycopy(data, 0, fullData, 1 + encodedFullLength.length, data.length);
return fullData;
}
public int getCount() {
return count;
}
}

View File

@ -0,0 +1,444 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import im.status.applet_installer_test.appletinstaller.*;
import org.spongycastle.crypto.engines.AESEngine;
import org.spongycastle.crypto.macs.CBCBlockCipherMac;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.jce.ECNamedCurveTable;
import org.spongycastle.jce.interfaces.ECPublicKey;
import org.spongycastle.jce.spec.ECParameterSpec;
import org.spongycastle.jce.spec.ECPublicKeySpec;
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.*;
import java.util.Arrays;
/**
* Handles a SecureChannel session with the card.
*/
public class SecureChannelSession {
public static final short SC_SECRET_LENGTH = 32;
public static final short SC_BLOCK_SIZE = 16;
public static final byte INS_OPEN_SECURE_CHANNEL = 0x10;
public static final byte INS_MUTUALLY_AUTHENTICATE = 0x11;
public static final byte INS_PAIR = 0x12;
public static final byte INS_UNPAIR = 0x13;
public static final byte PAIR_P1_FIRST_STEP = 0x00;
public static final byte PAIR_P1_LAST_STEP = 0x01;
public static final int PAYLOAD_MAX_SIZE = 223;
static final byte PAIRING_MAX_CLIENT_COUNT = 5;
private byte[] secret;
private byte[] publicKey;
private byte[] pairingKey;
private byte[] iv;
private byte pairingIndex;
private Cipher sessionCipher;
private CBCBlockCipherMac sessionMac;
private SecretKeySpec sessionEncKey;
private KeyParameter sessionMacKey;
private SecureRandom random;
private boolean open;
/**
* Constructs a SecureChannel session on the client. The client should generate a fresh key pair for each session.
* The public key of the card is used as input for the EC-DH algorithm. The output is stored as the secret.
*
* @param keyData the public key returned by the applet as response to the SELECT command
*/
public SecureChannelSession(byte[] keyData) {
try {
random = new SecureRandom();
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH");
g.initialize(ecSpec, random);
KeyPair keyPair = g.generateKeyPair();
publicKey = ((ECPublicKey) keyPair.getPublic()).getQ().getEncoded(false);
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH");
keyAgreement.init(keyPair.getPrivate());
ECPublicKeySpec cardKeySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(keyData), ecSpec);
ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA").generatePublic(cardKeySpec);
keyAgreement.doPhase(cardKey, true);
secret = keyAgreement.generateSecret();
open = false;
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
}
/**
* Returns the public key
* @return the public key
*/
public byte[] getPublicKey() {
return publicKey;
}
/**
* Returns the pairing index
* @return the pairing index
*/
public byte getPairingIndex() {
return pairingIndex;
}
/**
* Establishes a Secure Channel with the card. The command parameters are the public key generated in the first step.
* Follows the specifications from the SECURE_CHANNEL.md document.
*
* @param apduChannel the apdu channel
* @return the card response
* @throws IOException communication error
*/
public void autoOpenSecureChannel(CardChannel apduChannel) throws IOException {
APDUResponse response = openSecureChannel(apduChannel, pairingIndex, publicKey);
if (response.getSw() != 0x9000) {
throw new IOException("OPEN SECURE CHANNEL failed");
}
processOpenSecureChannelResponse(response);
response = mutuallyAuthenticate(apduChannel);
if (response.getSw() != 0x9000) {
throw new IOException("MUTUALLY AUTHENTICATE failed");
}
if(!verifyMutuallyAuthenticateResponse(response)) {
throw new IOException("Invalid authentication data from the card");
}
}
/**
* Processes the response from OPEN SECURE CHANNEL. This initialize the session keys, Cipher and MAC internally.
*
* @param response the card response
*/
public void processOpenSecureChannelResponse(APDUResponse response) {
try {
MessageDigest md = MessageDigest.getInstance("SHA512");
md.update(secret);
md.update(pairingKey);
byte[] data = response.getData();
byte[] keyData = md.digest(Arrays.copyOf(data, SC_SECRET_LENGTH));
iv = Arrays.copyOfRange(data, SC_SECRET_LENGTH, data.length);
sessionEncKey = new SecretKeySpec(Arrays.copyOf(keyData, SC_SECRET_LENGTH), "AES");
sessionMacKey = new KeyParameter(keyData, SC_SECRET_LENGTH, SC_SECRET_LENGTH);
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding");
sessionMac = new CBCBlockCipherMac(new AESEngine(), 128, null);
open = true;
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
}
/**
* Verify that the response from MUTUALLY AUTHENTICATE is correct.
*
* @param response the card response
* @return true if response is correct, false otherwise
*/
public boolean verifyMutuallyAuthenticateResponse(APDUResponse response) {
return response.getData().length == SC_SECRET_LENGTH;
}
/**
* Handles the entire pairing procedure in order to be able to use the secure channel
*
* @param apduChannel the apdu channel
* @throws IOException communication error
*/
public void autoPair(CardChannel apduChannel, byte[] sharedSecret) throws IOException {
byte[] challenge = new byte[32];
random.nextBytes(challenge);
APDUResponse resp = pair(apduChannel, PAIR_P1_FIRST_STEP, challenge);
if (resp.getSw() != 0x9000) {
throw new IOException("Pairing failed on step 1");
}
byte[] respData = resp.getData();
byte[] cardCryptogram = Arrays.copyOf(respData, 32);
byte[] cardChallenge = Arrays.copyOfRange(respData, 32, respData.length);
byte[] checkCryptogram;
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA256");
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
md.update(sharedSecret);
checkCryptogram = md.digest(challenge);
if (!Arrays.equals(checkCryptogram, cardCryptogram)) {
throw new IOException("Invalid card cryptogram");
}
md.update(sharedSecret);
checkCryptogram = md.digest(cardChallenge);
resp = pair(apduChannel, PAIR_P1_LAST_STEP, checkCryptogram);
if (resp.getSw() != 0x9000) {
throw new IOException("Pairing failed on step 2");
}
respData = resp.getData();
md.update(sharedSecret);
pairingKey = md.digest(Arrays.copyOfRange(respData, 1, respData.length));
pairingIndex = respData[0];
}
/**
* Unpairs the current paired key
*
* @param apduChannel the apdu channel
* @throws IOException communication error
*/
public void autoUnpair(CardChannel apduChannel) throws IOException {
APDUResponse resp = unpair(apduChannel, pairingIndex);
if (resp.getSw() != 0x9000) {
throw new IOException("Unpairing failed");
}
}
/**
* Sends a OPEN SECURE CHANNEL APDU.
*
* @param apduChannel the apdu channel
* @param index the P1 parameter
* @param data the data
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse openSecureChannel(CardChannel apduChannel, byte index, byte[] data) throws IOException {
open = false;
APDUCommand openSecureChannel = new APDUCommand(0x80, INS_OPEN_SECURE_CHANNEL, index, 0, data);
return apduChannel.send(openSecureChannel);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU. The data is generated automatically
*
* @param apduChannel the apdu channel
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse mutuallyAuthenticate(CardChannel apduChannel) throws IOException {
byte[] data = new byte[SC_SECRET_LENGTH];
random.nextBytes(data);
return mutuallyAuthenticate(apduChannel, data);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU.
*
* @param apduChannel the apdu channel
* @param data the data
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse mutuallyAuthenticate(CardChannel apduChannel, byte[] data) throws IOException {
APDUCommand mutuallyAuthenticate = protectedCommand(0x80, INS_MUTUALLY_AUTHENTICATE, 0, 0, data);
return transmit(apduChannel, mutuallyAuthenticate);
}
/**
* Sends a PAIR APDU.
*
* @param apduChannel the apdu channel
* @param p1 the P1 parameter
* @param data the data
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse pair(CardChannel apduChannel, byte p1, byte[] data) throws IOException {
APDUCommand openSecureChannel = new APDUCommand(0x80, INS_PAIR, p1, 0, data);
return transmit(apduChannel, openSecureChannel);
}
/**
* Sends a UNPAIR APDU.
*
* @param apduChannel the apdu channel
* @param p1 the P1 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse unpair(CardChannel apduChannel, byte p1) throws IOException {
APDUCommand openSecureChannel = protectedCommand(0x80, INS_UNPAIR, p1, 0, new byte[0]);
return transmit(apduChannel, openSecureChannel);
}
/**
* Unpair all other clients
*
* @param apduChannel the apdu channel
* @return the raw card response
* @throws IOException communication error
*/
public void unpairOthers(CardChannel apduChannel) throws IOException, APDUException {
for (int i = 0; i < PAIRING_MAX_CLIENT_COUNT; i++) {
if (i != pairingIndex) {
APDUCommand openSecureChannel = protectedCommand(0x80, INS_UNPAIR, i, 0, new byte[0]);
transmit(apduChannel, openSecureChannel).checkOK();
}
}
}
/**
* Encrypts the plaintext data using the session key. The maximum plaintext size is 223 bytes. The returned ciphertext
* already includes the IV and padding and can be sent as-is in the APDU payload. If the input is an empty byte array
* the returned data will still contain the IV and padding.
*
* @param data the plaintext data
* @return the encrypted data
*/
private byte[] encryptAPDU(byte[] data) {
assert data.length <= PAYLOAD_MAX_SIZE;
try {
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
sessionCipher.init(Cipher.ENCRYPT_MODE, sessionEncKey, ivParameterSpec);
return sessionCipher.doFinal(data);
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
}
/**
* Decrypts the response from the card using the session key. The returned data is already stripped from IV and padding
* and can be potentially empty.
*
* @param data the ciphetext
* @return the plaintext
*/
private byte[] decryptAPDU(byte[] data) {
try {
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
sessionCipher.init(Cipher.DECRYPT_MODE, sessionEncKey, ivParameterSpec);
return sessionCipher.doFinal(data);
} catch(Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
}
/**
* Returns a command APDU with MAC and encrypted data.
*
* @param cla the CLA byte
* @param ins the INS byte
* @param p1 the P1 byte
* @param p2 the P2 byte
* @param data the data, can be an empty array but not null
* @return the command APDU
*/
public APDUCommand protectedCommand(int cla, int ins, int p1, int p2, byte[] data) {
byte[] finalData;
if (open) {
data = encryptAPDU(data);
byte[] meta = new byte[]{(byte) cla, (byte) ins, (byte) p1, (byte) p2, (byte) (data.length + SC_BLOCK_SIZE), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
updateIV(meta, data);
finalData = Arrays.copyOf(iv, iv.length + data.length);
System.arraycopy(data, 0, finalData, iv.length, data.length);
} else {
finalData = data;
}
return new APDUCommand(cla, ins, p1, p2, finalData);
}
/**
* Transmits a protected command APDU and unwraps the response data. The MAC is verified, the data decrypted and the
* SW read from the payload.
*
* @param apduChannel the APDU channel
* @param apdu the APDU to send
* @return the unwrapped response APDU
* @throws IOException transmission error
*/
public APDUResponse transmit(CardChannel apduChannel, APDUCommand apdu) throws IOException {
APDUResponse resp = apduChannel.send(apdu);
if (resp.getSw() == 0x6982) {
open = false;
}
if (open) {
byte[] data = resp.getData();
byte[] meta = new byte[]{(byte) data.length, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
byte[] mac = Arrays.copyOf(data, iv.length);
data = Arrays.copyOfRange(data, iv.length, data.length);
byte[] plainData = decryptAPDU(data);
updateIV(meta, data);
if (!Arrays.equals(iv, mac)) {
throw new IOException("Invalid MAC");
}
return new APDUResponse(plainData);
} else {
return resp;
}
}
/**
* Marks the SecureChannel as closed
*/
public void reset() {
open = false;
}
/**
* Marks the SecureChannel as open. Only to be used when writing tests for the SecureChannel, in normal operation this
* would only make things wrong.
*
*/
void setOpen() {
open = true;
}
/**
* Calculates a CMAC from the metadata and data provided and sets it as the IV for the next message.
*
* @param meta metadata
* @param data data
*/
private void updateIV(byte[] meta, byte[] data) {
try {
sessionMac.init(sessionMacKey);
sessionMac.update(meta, 0, meta.length);
sessionMac.update(data, 0, data.length);
sessionMac.doFinal(iv, 0);
} catch (Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
}
}

View File

@ -0,0 +1,20 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
public class Select {
private static final int CLA = 0x00;
private static final int INS = 0xA4;
private static final int P1 = 0x04;
private static final int P2 = 0x00; // first occurrence
private byte[] aid;
public Select(byte[] aid) {
this.aid = aid;
}
public APDUCommand getCommand() {
return new APDUCommand(CLA, INS, P1, P2, this.aid);
}
}

View File

@ -0,0 +1,26 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
public class Status {
private static final int CLA = 0x80;
private static final int INS = 0xF2;
public static final int P1_ISSUER_SECURITY_DOMAIN = 0x80;
public static final int P1_APPLICATIONS = 0x40;
public static final int P1_EXECUTABLE_LOAD_FILES = 0x20;
public static final int P1_EXECUTABLE_LOAD_FILES_AND_MODULES = 0x10;
private static final int P2 = 0x02;
private int p1;
public Status(int p1) {
this.p1 = p1;
}
public APDUCommand getCommand() {
byte[] data = new byte[]{0x4F, 0x00};
return new APDUCommand(CLA, INS, this.p1, P2, data, true);
}
}

View File

@ -0,0 +1,441 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.APDUException;
import im.status.applet_installer_test.appletinstaller.APDUResponse;
import im.status.applet_installer_test.appletinstaller.CardChannel;
import org.spongycastle.jce.interfaces.ECPrivateKey;
import org.spongycastle.jce.interfaces.ECPublicKey;
import org.spongycastle.util.encoders.Hex;
import java.io.IOException;
import java.security.KeyPair;
import java.security.PrivateKey;
/**
* This class is used to send APDU to the applet. Each method corresponds to an APDU as defined in the APPLICATION.md
* file. Some APDUs map to multiple methods for the sake of convenience since their payload or response require some
* pre/post processing.
*/
public class WalletAppletCommandSet {
static final byte INS_GET_STATUS = (byte) 0xF2;
static final byte INS_VERIFY_PIN = (byte) 0x20;
static final byte INS_CHANGE_PIN = (byte) 0x21;
static final byte INS_UNBLOCK_PIN = (byte) 0x22;
static final byte INS_LOAD_KEY = (byte) 0xD0;
static final byte INS_DERIVE_KEY = (byte) 0xD1;
static final byte INS_GENERATE_MNEMONIC = (byte) 0xD2;
static final byte INS_SIGN = (byte) 0xC0;
static final byte INS_SET_PINLESS_PATH = (byte) 0xC1;
static final byte INS_EXPORT_KEY = (byte) 0xC2;
static final byte GET_STATUS_P1_APPLICATION = 0x00;
static final byte LOAD_KEY_P1_EC = 0x01;
static final byte LOAD_KEY_P1_EXT_EC = 0x02;
static final byte LOAD_KEY_P1_SEED = 0x03;
static final byte DERIVE_P1_ASSISTED_MASK = 0x01;
static final byte DERIVE_P1_SOURCE_MASTER = (byte) 0x00;
static final byte DERIVE_P2_KEY_PATH = 0x00;
static final byte DERIVE_P2_PUBLIC_KEY = 0x01;
static final byte EXPORT_KEY_P2_PRIVATE_AND_PUBLIC = 0x00;
static final byte EXPORT_KEY_P2_PUBLIC_ONLY = 0x01;
static final byte TLV_PUB_KEY = (byte) 0x80;
static final byte TLV_PRIV_KEY = (byte) 0x81;
static final byte TLV_CHAIN_CODE = (byte) 0x82;
public static final String APPLET_AID = "53746174757357616C6C6574417070";
public static final byte[] APPLET_AID_BYTES = Hex.decode(APPLET_AID);
private final CardChannel apduChannel;
private SecureChannelSession secureChannel;
public WalletAppletCommandSet(CardChannel apduChannel) {
this.apduChannel = apduChannel;
}
public void setSecureChannel(SecureChannelSession secureChannel) {
this.secureChannel = secureChannel;
}
/**
* Selects the applet. The applet is assumed to have been installed with its default AID. The returned data is a
* public key which must be used to initialize the secure channel.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse select() throws IOException {
if (secureChannel != null) {
secureChannel.reset();
}
APDUCommand selectApplet = new APDUCommand(0x00, 0xA4, 4, 0, APPLET_AID_BYTES);
return apduChannel.send(selectApplet);
}
/**
* Opens the secure channel. Calls the corresponding method of the SecureChannel class.
*
* @return the raw card response
* @throws IOException communication error
*/
public void autoOpenSecureChannel() throws IOException {
secureChannel.autoOpenSecureChannel(apduChannel);
}
/**
* Automatically pairs. Calls the corresponding method of the SecureChannel class.
*
* @throws IOException communication error
*/
public void autoPair(byte[] sharedSecret) throws IOException {
secureChannel.autoPair(apduChannel, sharedSecret);
}
/**
* Automatically unpairs. Calls the corresponding method of the SecureChannel class.
*
* @throws IOException communication error
*/
public void autoUnpair() throws IOException {
secureChannel.autoUnpair(apduChannel);
}
/**
* Sends a OPEN SECURE CHANNEL APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse openSecureChannel(byte index, byte[] data) throws IOException {
return secureChannel.openSecureChannel(apduChannel, index, data);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse mutuallyAuthenticate() throws IOException {
return secureChannel.mutuallyAuthenticate(apduChannel);
}
/**
* Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse mutuallyAuthenticate(byte[] data) throws IOException {
return secureChannel.mutuallyAuthenticate(apduChannel, data);
}
/**
* Sends a PAIR APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse pair(byte p1, byte[] data) throws IOException {
return secureChannel.pair(apduChannel, p1, data);
}
/**
* Sends a UNPAIR APDU. Calls the corresponding method of the SecureChannel class.
*/
public APDUResponse unpair(byte p1) throws IOException {
return secureChannel.unpair(apduChannel, p1);
}
/**
* Unpair all other clients.
*/
public void unpairOthers() throws IOException, APDUException {
secureChannel.unpairOthers(apduChannel);
}
/**
* Sends a GET STATUS APDU. The info byte is the P1 parameter of the command, valid constants are defined in the applet
* class itself.
*
* @param info the P1 of the APDU
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse getStatus(byte info) throws IOException {
APDUCommand getStatus = secureChannel.protectedCommand(0x80, INS_GET_STATUS, info, 0, new byte[0]);
return secureChannel.transmit(apduChannel, getStatus);
}
/**
* Sends a GET STATUS APDU to retrieve the APPLICATION STATUS template and reads the byte indicating public key
* derivation support.
*
* @return whether public key derivation is supported or not
* @throws IOException communication error
*/
public boolean getPublicKeyDerivationSupport() throws IOException {
APDUResponse resp = getStatus(GET_STATUS_P1_APPLICATION);
byte[] data = resp.getData();
return data[data.length - 1] != 0x00;
}
/**
* Sends a VERIFY PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
* data.
*
* @param pin the pin
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse verifyPIN(String pin) throws IOException {
APDUCommand verifyPIN = secureChannel.protectedCommand(0x80, INS_VERIFY_PIN, 0, 0, pin.getBytes());
return secureChannel.transmit(apduChannel, verifyPIN);
}
/**
* Sends a CHANGE PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
* data.
*
* @param pin the new PIN
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse changePIN(String pin) throws IOException {
APDUCommand changePIN = secureChannel.protectedCommand(0x80, INS_CHANGE_PIN, 0, 0, pin.getBytes());
return secureChannel.transmit(apduChannel, changePIN);
}
/**
* Sends an UNBLOCK PIN APDU. The PUK and PIN are concatenated and the raw bytes are encrypted using the secure
* channel and used as APDU data.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse unblockPIN(String puk, String newPin) throws IOException {
APDUCommand unblockPIN = secureChannel.protectedCommand(0x80, INS_UNBLOCK_PIN, 0, 0, (puk + newPin).getBytes());
return secureChannel.transmit(apduChannel, unblockPIN);
}
/**
* Sends a LOAD KEY APDU. The given private key and chain code are formatted as a raw binary seed and the P1 of
* the command is set to LOAD_KEY_P1_SEED (0x03). This works on cards which support public key derivation.
* The loaded keyset is extended and support further key derivation.
*
* @param aPrivate a private key
* @param chainCode the chain code
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(PrivateKey aPrivate, byte[] chainCode) throws IOException {
byte[] privateKey = ((ECPrivateKey) aPrivate).getD().toByteArray();
int privLen = privateKey.length;
int privOff = 0;
if(privateKey[0] == 0x00) {
privOff++;
privLen--;
}
byte[] data = new byte[chainCode.length + privLen];
System.arraycopy(privateKey, privOff, data, 0, privLen);
System.arraycopy(chainCode, 0, data, privLen, chainCode.length);
return loadKey(data, LOAD_KEY_P1_SEED);
}
/**
* Sends a LOAD KEY APDU. The key is sent in TLV format, includes the public key and no chain code, meaning that
* the card will not be able to do further key derivation.
*
* @param ecKeyPair a key pair
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(KeyPair ecKeyPair) throws IOException {
return loadKey(ecKeyPair, false, null);
}
/**
* Sends a LOAD KEY APDU. The key is sent in TLV format. The public key is included or not depending on the value
* of the omitPublicKey parameter. The chain code is included if the chainCode is not null. P1 is set automatically
* to either LOAD_KEY_P1_EC or LOAD_KEY_P1_EXT_EC depending on the presence of the chainCode.
*
* @param keyPair a key pair
* @param omitPublicKey whether the public key is sent or not
* @param chainCode the chain code
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(KeyPair keyPair, boolean omitPublicKey, byte[] chainCode) throws IOException {
byte[] publicKey = omitPublicKey ? null : ((ECPublicKey) keyPair.getPublic()).getQ().getEncoded(false);
byte[] privateKey = ((ECPrivateKey) keyPair.getPrivate()).getD().toByteArray();
return loadKey(publicKey, privateKey, chainCode);
}
/**
* Sends a LOAD KEY APDU. The key is sent in TLV format. The public key is included if not null. The chain code is
* included if not null. P1 is set automatically to either LOAD_KEY_P1_EC or
* LOAD_KEY_P1_EXT_EC depending on the presence of the chainCode.
*
* @param publicKey a raw public key
* @param privateKey a raw private key
* @param chainCode the chain code
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(byte[] publicKey, byte[] privateKey, byte[] chainCode) throws IOException {
int privLen = privateKey.length;
int privOff = 0;
if(privateKey[0] == 0x00) {
privOff++;
privLen--;
}
int off = 0;
int totalLength = publicKey == null ? 0 : (publicKey.length + 2);
totalLength += (privLen + 2);
totalLength += chainCode == null ? 0 : (chainCode.length + 2);
if (totalLength > 127) {
totalLength += 3;
} else {
totalLength += 2;
}
byte[] data = new byte[totalLength];
data[off++] = (byte) 0xA1;
if (totalLength > 127) {
data[off++] = (byte) 0x81;
data[off++] = (byte) (totalLength - 3);
} else {
data[off++] = (byte) (totalLength - 2);
}
if (publicKey != null) {
data[off++] = TLV_PUB_KEY;
data[off++] = (byte) publicKey.length;
System.arraycopy(publicKey, 0, data, off, publicKey.length);
off += publicKey.length;
}
data[off++] = TLV_PRIV_KEY;
data[off++] = (byte) privLen;
System.arraycopy(privateKey, privOff, data, off, privLen);
off += privLen;
byte p1;
if (chainCode != null) {
p1 = LOAD_KEY_P1_EXT_EC;
data[off++] = (byte) TLV_CHAIN_CODE;
data[off++] = (byte) chainCode.length;
System.arraycopy(chainCode, 0, data, off, chainCode.length);
} else {
p1 = LOAD_KEY_P1_EC;
}
return loadKey(data, p1);
}
/**
* Sends a LOAD KEY APDU. The data is encrypted and sent as-is. The keyType parameter is used as P1.
*
* @param data key data
* @param keyType the P1 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse loadKey(byte[] data, byte keyType) throws IOException {
APDUCommand loadKey = secureChannel.protectedCommand(0x80, INS_LOAD_KEY, keyType, 0, data);
return secureChannel.transmit(apduChannel, loadKey);
}
/**
* Sends a GENERATE MNEMONIC APDU. The cs parameter is the length of the checksum and is used as P1.
*
* @param cs the P1 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse generateMnemonic(int cs) throws IOException {
APDUCommand generateMnemonic = secureChannel.protectedCommand(0x80, INS_GENERATE_MNEMONIC, cs, 0, new byte[0]);
return secureChannel.transmit(apduChannel, generateMnemonic);
}
/**
* Sends a SIGN APDU. The dataType is P1 as defined in the applet. The isFirst and isLast arguments are used to form
* the P2 parameter. The data is the data to sign, or part of it. Only when sending the last block a signature is
* generated and thus returned. When signing a precomputed hash it must be done in a single block, so isFirst and
* isLast will always be true at the same time.
*
* @param data the data to sign
* @param dataType the P1 parameter
* @param isFirst whether this is the first block of the command or not
* @param isLast whether this is the last block of the command or not
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse sign(byte[] data, byte dataType, boolean isFirst, boolean isLast) throws IOException {
byte p2 = (byte) ((isFirst ? 0x01 : 0x00) | (isLast ? 0x80 : 0x00));
APDUCommand sign = secureChannel.protectedCommand(0x80, INS_SIGN, dataType, p2, data);
return secureChannel.transmit(apduChannel, sign);
}
/**
* Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The P1 and P2 parameters are forced to 0, meaning
* that the derivation starts from the master key and is non-assisted.
*
* @param data the raw key path
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse deriveKey(byte[] data) throws IOException {
return deriveKey(data, DERIVE_P1_SOURCE_MASTER, false, false);
}
/**
* Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The reset and assisted parameters are combined to
* form P1. The isPublicKey parameter is used for P2.
*
* @param data the raw key path or a public key
* @param source the source to start derivation
* @param assisted whether we are doing assisted derivation or not
* @param isPublicKey whether we are sending a public key or a key path (only make sense during assisted derivation)
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse deriveKey(byte[] data, int source, boolean assisted, boolean isPublicKey) throws IOException {
byte p1 = assisted ? DERIVE_P1_ASSISTED_MASK : 0;
p1 |= source;
byte p2 = isPublicKey ? DERIVE_P2_PUBLIC_KEY : DERIVE_P2_KEY_PATH;
APDUCommand deriveKey = secureChannel.protectedCommand(0x80, INS_DERIVE_KEY, p1, p2, data);
return secureChannel.transmit(apduChannel, deriveKey);
}
/**
* Sends a SET PINLESS PATH APDU. The data is encrypted and sent as-is.
*
* @param data the raw key path
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse setPinlessPath(byte [] data) throws IOException {
APDUCommand setPinlessPath = secureChannel.protectedCommand(0x80, INS_SET_PINLESS_PATH, 0x00, 0x00, data);
return secureChannel.transmit(apduChannel, setPinlessPath);
}
/**
* Sends an EXPORT KEY APDU. The keyPathIndex is used as P1. Valid values are defined in the applet itself
*
* @param keyPathIndex the P1 parameter
* @param publicOnly the P2 parameter
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse exportKey(byte keyPathIndex, boolean publicOnly) throws IOException {
byte p2 = publicOnly ? EXPORT_KEY_P2_PUBLIC_ONLY : EXPORT_KEY_P2_PRIVATE_AND_PUBLIC;
APDUCommand exportKey = secureChannel.protectedCommand(0x80, INS_EXPORT_KEY, keyPathIndex, p2, new byte[0]);
return secureChannel.transmit(apduChannel, exportKey);
}
}

View File

@ -0,0 +1,21 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import org.junit.Test;
import org.spongycastle.util.encoders.Hex;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import static org.junit.Assert.*;
public class DeleteTest {
@Test
public void getCommand() throws IOException {
byte[] aid = HexUtils.hexStringToByteArray("53746174757357616C6C6574");
Delete delete = new Delete(aid);
String expected = "80E400800E4F0C53746174757357616C6C6574";
byte[] apdu = delete.getCommand().serialize();
assertEquals(expected, HexUtils.byteArrayToHexString(apdu));
}
}

View File

@ -0,0 +1,38 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import org.junit.Test;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import static org.junit.Assert.*;
public class ExternalAuthenticateTest {
@Test
public void getHostCryptogram() {
byte[] encKeyData = HexUtils.hexStringToByteArray("0EF72A1065236DD6CAC718D5E3F379A4");
byte[] cardChallenge = HexUtils.hexStringToByteArray("0076a6c0d55e9535");
byte[] hostChallenge = HexUtils.hexStringToByteArray("266195e638da1b95");
ExternalAuthenticate auth = new ExternalAuthenticate(encKeyData, cardChallenge, hostChallenge);
String expectedHostCryptogram = "45A5F48DAE68203C";
byte[] hostCryptogram = auth.getHostCryptogram();
assertEquals(expectedHostCryptogram, HexUtils.byteArrayToHexString(hostCryptogram));
}
@Test
public void getCommand() throws IOException {
byte[] encKeyData = HexUtils.hexStringToByteArray("8D289AFE0AB9C45B1C76DEEA182966F4");
byte[] cardChallenge = HexUtils.hexStringToByteArray("000f3fd65d4d6e45");
byte[] hostChallenge = HexUtils.hexStringToByteArray("cf307b6719bf224d");
ExternalAuthenticate auth = new ExternalAuthenticate(encKeyData, cardChallenge, hostChallenge);
String expectedAPDU = "84820100087702AC6CE46A47F0";
byte[] apdu = auth.getCommand().serialize();
assertEquals(expectedAPDU, HexUtils.byteArrayToHexString(apdu));
}
}

View File

@ -0,0 +1,62 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import org.junit.Test;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.APDUException;
import im.status.applet_installer_test.appletinstaller.APDUResponse;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import im.status.applet_installer_test.appletinstaller.Keys;
import static org.junit.Assert.*;
public class InitializeUpdateTest {
@Test
public void getCommand() throws IOException {
byte[] challenge = HexUtils.hexStringToByteArray("2d315d5ffc616d10");
InitializeUpdate init = new InitializeUpdate(challenge);
APDUCommand cmd = init.getCommand();
assertEquals(0x80, cmd.getCla());
assertEquals(0x50, cmd.getIns());
assertEquals(0, cmd.getP1());
assertEquals(0, cmd.getP2());
assertEquals(challenge, cmd.getData());
String expectedAPDU = "80500000082D315D5FFC616D1000";
byte[] apdu = cmd.serialize();
assertEquals(expectedAPDU, HexUtils.byteArrayToHexString(apdu));
}
@Test
public void validateResponse_BadResponse() throws APDUException {
byte[] apdu = HexUtils.hexStringToByteArray("000002650183039536622002003b5e508f751c0af3016e3fbc23d3a66982");
APDUResponse resp = new APDUResponse(apdu);
byte[] challenge = InitializeUpdate.generateChallenge();
InitializeUpdate init = new InitializeUpdate(challenge);
try {
init.verifyResponse(new Keys(new byte[]{}, new byte[]{}), resp);
fail("expected APDUException to be thrown");
} catch (APDUException e) {
assertEquals(0x6982, e.sw);
}
}
//TODO: reimplement test
//@Test
//public void validateResponse_GoodResponse() throws APDUException {
// byte[] encKey = HexUtils.hexStringToByteArray("16B5867FF50BE7239C2BF1245B83A362");
// byte[] challenge = HexUtils.hexStringToByteArray("f0467f908e5ca23f");
// InitializeUpdate init = new InitializeUpdate(challenge);
// byte[] apdu = HexUtils.hexStringToByteArray("000002650183039536622002000de9c62ba1c4c8e55fcb91b6654ce49000");
// APDUResponse resp = new APDUResponse(apdu);
// init.verifyResponse(new Keys(), resp);
//}
}

View File

@ -0,0 +1,26 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import org.junit.Test;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.APDUCommandTest;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import static org.junit.Assert.*;
public class InstallForInstallTest {
@Test
public void forInstall() throws IOException {
byte[] packageAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574");
byte[] appletAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574417070");
byte[] instanceAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574417070");
byte[] params = HexUtils.hexStringToByteArray("AABBCC");
InstallForInstall install = new InstallForInstall(packageAID, appletAID, instanceAID, params);
APDUCommand cmd = install.getCommand();
byte[] apdu = cmd.serialize();
String expected = "80E60C00360C53746174757357616C6C65740F53746174757357616C6C65744170700F53746174757357616C6C6574417070010005C903AABBCC00";
assertEquals(expected, HexUtils.byteArrayToHexString(apdu));
}
}

View File

@ -0,0 +1,24 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import org.junit.Test;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import static org.junit.Assert.*;
public class InstallForLoadTest {
@Test
public void forLoad() throws IOException {
byte[] aid = HexUtils.hexStringToByteArray("53746174757357616C6C6574");
byte[] sdaid = HexUtils.hexStringToByteArray("A000000151000000");
InstallForLoad install = new InstallForLoad(aid, sdaid);
APDUCommand cmd = install.getCommand();
byte[] apdu = cmd.serialize();
String expected = "80E60200190C53746174757357616C6C657408A000000151000000000000";
assertEquals(expected, HexUtils.byteArrayToHexString(apdu));
}
}

View File

@ -0,0 +1,53 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import org.junit.Test;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import static org.junit.Assert.*;
public class LoadTest {
@Test
public void getCommand() throws IOException {
URL url = this.getClass().getClassLoader().getResource("wallet.cap");
Load load = new Load(url.getPath());
ArrayList<APDUCommand> commands = new ArrayList<APDUCommand>();
APDUCommand cmd;
while((cmd = load.getCommand()) != null) {
commands.add(cmd);
}
assertEquals(31, commands.size());
// Command 1
cmd = commands.get(0);
assertEquals(0, cmd.getP1());
assertEquals(0, cmd.getP2());
String expectedData = "C4821D74010027DECAFFED02020401010C53746174757357616C6C657410696D2F7374617475732F77616C6C6574020021002700210013002902B600401581015D0301000006EB372C0024000A013504010004002904000107A0000000620001050107A0000000620102050107A0000000620101050107A0000000620201030013010F53746174757357616C6C65744170700937060040000000800000FF000100000000800000FF00010000000080000D000A010A000004EB05A005F307140739080C08D608EB0908090D008203160010070100000A7B07158106005A801A0076003003808009038B00300457800904620030051F8010";
byte[] data = cmd.getData();
assertEquals(expectedData, HexUtils.byteArrayToHexString(data));
// Command 2
cmd = commands.get(1);
assertEquals(0, cmd.getP1());
assertEquals(1, cmd.getP2());
expectedData = "053100440A9380AB0B4000750EBF80930F5400300110188C002F7A0302058D00327F003307038D00347F003506038D00377F00381006038D00347F003B032F101B038D00407F004110141020038D0042940000437F005E700B2C017F00411100802F10651C41048D005F7F00607A0861032906181D2510805310806B1E7B00601606590601033816061A7B006016068E03006113412906702B1B7B0060038E03006213044329067B0060037B00601606250453600506700305381606054704412906181D7B00601606078D006329061504160510207B00600316067B006016068D00647B006016067B0065038D0066620403781A7B0060";
data = cmd.getData();
assertEquals(expectedData, HexUtils.byteArrayToHexString(data));
// Last command
cmd = commands.get(30);
assertEquals(0x80, cmd.getP1());
assertEquals(30, cmd.getP2());
expectedData = "080707080C08090A04050A0A06070706081A07085029031512201209190C0B0B0503060D09030E0B0C0C080A0A0603070706104622070914240A1611130D080B0F0A0D10110905040B2E271205030E0D0D0D0D0807140808030E1E10050321032C2A070606080D140C2723180B081D0A0707060811030D0407070608201B07091408252C2E39";
data = cmd.getData();
assertEquals(expectedData, HexUtils.byteArrayToHexString(data));
}
}

View File

@ -0,0 +1,20 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import org.junit.Test;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import static org.junit.Assert.*;
public class SelectTest {
@Test
public void getCode() throws IOException {
Select s = new Select(new byte[0]);
byte[] apdu = s.getCommand().serialize();
String expected = "00A4040000";
assertEquals(expected, HexUtils.byteArrayToHexString(apdu));
}
}

View File

@ -0,0 +1,21 @@
package im.status.applet_installer_test.appletinstaller.apducommands;
import org.junit.Test;
import java.io.IOException;
import im.status.applet_installer_test.appletinstaller.APDUCommand;
import im.status.applet_installer_test.appletinstaller.APDUWrapper;
import im.status.applet_installer_test.appletinstaller.HexUtils;
import static org.junit.Assert.*;
public class StatusTest {
@Test
public void getCommand() throws IOException {
Status status = new Status(Status.P1_ISSUER_SECURITY_DOMAIN);
String expectedAPDU = "80F28002024F0000";
byte[] apdu = status.getCommand().serialize();
assertEquals(expectedAPDU, HexUtils.byteArrayToHexString(apdu));
}
}