mirror of
https://github.com/status-im/status-keycard.git
synced 2025-03-03 22:30:36 +00:00
Closes #17
This commit is contained in:
parent
846ab1443b
commit
c5a398e05e
@ -34,6 +34,7 @@ javacard {
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@ -41,6 +42,7 @@ dependencies {
|
||||
testCompile('org.web3j:core:2.3.1')
|
||||
testCompile('org.bitcoinj:bitcoinj-core:0.14.5')
|
||||
testCompile("org.bouncycastle:bcprov-jdk15on:1.58")
|
||||
testCompile("com.github.status-im:hardwallet-lite-sdk:7e3787f")
|
||||
testCompile("org.junit.jupiter:junit-jupiter-api:5.1.1")
|
||||
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.1.1")
|
||||
}
|
||||
|
@ -1,447 +0,0 @@
|
||||
package im.status.wallet;
|
||||
|
||||
import org.bouncycastle.crypto.engines.AESEngine;
|
||||
import org.bouncycastle.crypto.macs.CBCBlockCipherMac;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
import org.bouncycastle.jce.ECNamedCurveTable;
|
||||
import org.bouncycastle.jce.interfaces.ECPublicKey;
|
||||
import org.bouncycastle.jce.spec.ECParameterSpec;
|
||||
import org.bouncycastle.jce.spec.ECPublicKeySpec;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyAgreement;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.smartcardio.CardChannel;
|
||||
import javax.smartcardio.CardException;
|
||||
import javax.smartcardio.CommandAPDU;
|
||||
import javax.smartcardio.ResponseAPDU;
|
||||
import java.security.*;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Handles a SecureChannel session with the card.
|
||||
*/
|
||||
public class SecureChannelSession {
|
||||
public static final int PAYLOAD_MAX_SIZE = 223;
|
||||
|
||||
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) {
|
||||
random = new SecureRandom();
|
||||
generateSecret(keyData);
|
||||
open = false;
|
||||
}
|
||||
|
||||
public void generateSecret(byte[] keyData) {
|
||||
try {
|
||||
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
|
||||
KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH", "BC");
|
||||
g.initialize(ecSpec, random);
|
||||
|
||||
KeyPair keyPair = g.generateKeyPair();
|
||||
|
||||
publicKey = ((ECPublicKey) keyPair.getPublic()).getQ().getEncoded(false);
|
||||
KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH", "BC");
|
||||
keyAgreement.init(keyPair.getPrivate());
|
||||
|
||||
ECPublicKeySpec cardKeySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(keyData), ecSpec);
|
||||
ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA", "BC").generatePublic(cardKeySpec);
|
||||
|
||||
keyAgreement.doPhase(cardKey, true);
|
||||
secret = keyAgreement.generateSecret();
|
||||
} 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 CardException communication error
|
||||
*/
|
||||
public void autoOpenSecureChannel(CardChannel apduChannel) throws CardException {
|
||||
ResponseAPDU response = openSecureChannel(apduChannel, pairingIndex, publicKey);
|
||||
|
||||
if (response.getSW() != 0x9000) {
|
||||
throw new CardException("OPEN SECURE CHANNEL failed");
|
||||
}
|
||||
|
||||
processOpenSecureChannelResponse(response);
|
||||
|
||||
response = mutuallyAuthenticate(apduChannel);
|
||||
|
||||
if (response.getSW() != 0x9000) {
|
||||
throw new CardException("MUTUALLY AUTHENTICATE failed");
|
||||
}
|
||||
|
||||
if(!verifyMutuallyAuthenticateResponse(response)) {
|
||||
throw new CardException("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(ResponseAPDU response) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA512", "BC");
|
||||
md.update(secret);
|
||||
md.update(pairingKey);
|
||||
byte[] data = response.getData();
|
||||
byte[] keyData = md.digest(Arrays.copyOf(data, SecureChannel.SC_SECRET_LENGTH));
|
||||
iv = Arrays.copyOfRange(data, SecureChannel.SC_SECRET_LENGTH, data.length);
|
||||
|
||||
sessionEncKey = new SecretKeySpec(Arrays.copyOf(keyData, SecureChannel.SC_SECRET_LENGTH), "AES");
|
||||
sessionMacKey = new KeyParameter(keyData, SecureChannel.SC_SECRET_LENGTH, SecureChannel.SC_SECRET_LENGTH);
|
||||
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding", "BC");
|
||||
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(ResponseAPDU response) {
|
||||
return response.getNr() == SecureChannel.SC_SECRET_LENGTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the entire pairing procedure in order to be able to use the secure channel
|
||||
*
|
||||
* @param apduChannel the apdu channel
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public void autoPair(CardChannel apduChannel, byte[] sharedSecret) throws CardException {
|
||||
byte[] challenge = new byte[32];
|
||||
random.nextBytes(challenge);
|
||||
ResponseAPDU resp = pair(apduChannel, SecureChannel.PAIR_P1_FIRST_STEP, challenge);
|
||||
|
||||
if (resp.getSW() != 0x9000) {
|
||||
throw new CardException("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", "BC");
|
||||
} 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 CardException("Invalid card cryptogram");
|
||||
}
|
||||
|
||||
md.update(sharedSecret);
|
||||
checkCryptogram = md.digest(cardChallenge);
|
||||
|
||||
resp = pair(apduChannel, SecureChannel.PAIR_P1_LAST_STEP, checkCryptogram);
|
||||
|
||||
if (resp.getSW() != 0x9000) {
|
||||
throw new CardException("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 CardException communication error
|
||||
*/
|
||||
public void autoUnpair(CardChannel apduChannel) throws CardException {
|
||||
ResponseAPDU resp = unpair(apduChannel, pairingIndex);
|
||||
|
||||
if (resp.getSW() != 0x9000) {
|
||||
throw new CardException("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 CardException communication error
|
||||
*/
|
||||
public ResponseAPDU openSecureChannel(CardChannel apduChannel, byte index, byte[] data) throws CardException {
|
||||
open = false;
|
||||
CommandAPDU openSecureChannel = new CommandAPDU(0x80, SecureChannel.INS_OPEN_SECURE_CHANNEL, index, 0, data);
|
||||
return apduChannel.transmit(openSecureChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a MUTUALLY AUTHENTICATE APDU. The data is generated automatically
|
||||
*
|
||||
* @param apduChannel the apdu channel
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU mutuallyAuthenticate(CardChannel apduChannel) throws CardException {
|
||||
byte[] data = new byte[SecureChannel.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 CardException communication error
|
||||
*/
|
||||
public ResponseAPDU mutuallyAuthenticate(CardChannel apduChannel, byte[] data) throws CardException {
|
||||
CommandAPDU mutuallyAuthenticate = protectedCommand(0x80, SecureChannel.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 CardException communication error
|
||||
*/
|
||||
public ResponseAPDU pair(CardChannel apduChannel, byte p1, byte[] data) throws CardException {
|
||||
CommandAPDU openSecureChannel = new CommandAPDU(0x80, SecureChannel.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 CardException communication error
|
||||
*/
|
||||
public ResponseAPDU unpair(CardChannel apduChannel, byte p1) throws CardException {
|
||||
CommandAPDU openSecureChannel = protectedCommand(0x80, SecureChannel.INS_UNPAIR, p1, 0, new byte[0]);
|
||||
return transmit(apduChannel, openSecureChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 CommandAPDU 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 + SecureChannel.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 CommandAPDU(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 CardException transmission error
|
||||
*/
|
||||
public ResponseAPDU transmit(CardChannel apduChannel, CommandAPDU apdu) throws CardException {
|
||||
ResponseAPDU resp = apduChannel.transmit(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 CardException("Invalid MAC");
|
||||
}
|
||||
|
||||
return new ResponseAPDU(plainData);
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the SecureChannel as closed
|
||||
*/
|
||||
public void reset() {
|
||||
open = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the payload for the INIT command
|
||||
* @param initData the payload for the INIT command
|
||||
*
|
||||
* @return the encrypted buffer
|
||||
*/
|
||||
public byte[] oneShotEncrypt(byte[] initData) {
|
||||
try {
|
||||
iv = new byte[SecureChannel.SC_BLOCK_SIZE];
|
||||
random.nextBytes(iv);
|
||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
|
||||
sessionEncKey = new SecretKeySpec(secret, "AES");
|
||||
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding", "BC");
|
||||
sessionCipher.init(Cipher.ENCRYPT_MODE, sessionEncKey, ivParameterSpec);
|
||||
initData = sessionCipher.doFinal(initData);
|
||||
byte[] encrypted = new byte[1 + publicKey.length + iv.length + initData.length];
|
||||
encrypted[0] = (byte) publicKey.length;
|
||||
System.arraycopy(publicKey, 0, encrypted, 1, publicKey.length);
|
||||
System.arraycopy(iv, 0, encrypted, (1 + publicKey.length), iv.length);
|
||||
System.arraycopy(initData, 0, encrypted, (1 + publicKey.length + iv.length), initData.length);
|
||||
return encrypted;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package im.status.wallet;
|
||||
|
||||
import im.status.hardwallet.lite.SecureChannelSession;
|
||||
|
||||
public class TestSecureChannelSession extends SecureChannelSession {
|
||||
public void setOpen() {
|
||||
super.setOpen();
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package im.status.wallet;
|
||||
|
||||
import im.status.hardwallet.lite.WalletAppletCommandSet;
|
||||
import org.web3j.crypto.ECKeyPair;
|
||||
|
||||
import javax.smartcardio.CardChannel;
|
||||
import javax.smartcardio.CardException;
|
||||
import javax.smartcardio.ResponseAPDU;
|
||||
|
||||
public class TestWalletAppletCommandSet extends WalletAppletCommandSet {
|
||||
public TestWalletAppletCommandSet(CardChannel apduChannel) {
|
||||
super(apduChannel);
|
||||
}
|
||||
|
||||
public void setSecureChannel(TestSecureChannelSession secureChannel) {
|
||||
super.setSecureChannel(secureChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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. This is needed when the argument is an EC keypair from
|
||||
* the web3j package instead of the regular Java ones. Used by the test which actually submits the transaction to
|
||||
* the network.
|
||||
*
|
||||
* @param ecKeyPair a key pair
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU loadKey(ECKeyPair ecKeyPair) throws CardException {
|
||||
byte[] publicKey = ecKeyPair.getPublicKey().toByteArray();
|
||||
byte[] privateKey = ecKeyPair.getPrivateKey().toByteArray();
|
||||
|
||||
int pubLen = publicKey.length;
|
||||
int pubOff = 0;
|
||||
|
||||
if(publicKey[0] == 0x00) {
|
||||
pubOff++;
|
||||
pubLen--;
|
||||
}
|
||||
|
||||
byte[] ansiPublic = new byte[pubLen + 1];
|
||||
ansiPublic[0] = 0x04;
|
||||
System.arraycopy(publicKey, pubOff, ansiPublic, 1, pubLen);
|
||||
|
||||
return loadKey(ansiPublic, privateKey, null);
|
||||
}
|
||||
}
|
||||
|
@ -1,487 +0,0 @@
|
||||
package im.status.wallet;
|
||||
|
||||
import com.licel.jcardsim.utils.ByteUtil;
|
||||
import javacard.framework.ISO7816;
|
||||
import org.bouncycastle.jce.interfaces.ECPrivateKey;
|
||||
import org.bouncycastle.jce.interfaces.ECPublicKey;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.web3j.crypto.ECKeyPair;
|
||||
|
||||
import javax.smartcardio.CardChannel;
|
||||
import javax.smartcardio.CardException;
|
||||
import javax.smartcardio.CommandAPDU;
|
||||
import javax.smartcardio.ResponseAPDU;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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 CardException communication error
|
||||
*/
|
||||
public ResponseAPDU select() throws CardException {
|
||||
if (secureChannel != null) {
|
||||
secureChannel.reset();
|
||||
}
|
||||
|
||||
CommandAPDU selectApplet = new CommandAPDU(ISO7816.CLA_ISO7816, ISO7816.INS_SELECT, 4, 0, APPLET_AID_BYTES);
|
||||
return apduChannel.transmit(selectApplet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the secure channel. Calls the corresponding method of the SecureChannel class.
|
||||
*
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public void autoOpenSecureChannel() throws CardException {
|
||||
secureChannel.autoOpenSecureChannel(apduChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically pairs. Calls the corresponding method of the SecureChannel class.
|
||||
*
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public void autoPair(byte[] sharedSecret) throws CardException {
|
||||
secureChannel.autoPair(apduChannel, sharedSecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically unpairs. Calls the corresponding method of the SecureChannel class.
|
||||
*
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public void autoUnpair() throws CardException {
|
||||
secureChannel.autoUnpair(apduChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a OPEN SECURE CHANNEL APDU. Calls the corresponding method of the SecureChannel class.
|
||||
*/
|
||||
public ResponseAPDU openSecureChannel(byte index, byte[] data) throws CardException {
|
||||
return secureChannel.openSecureChannel(apduChannel, index, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class.
|
||||
*/
|
||||
public ResponseAPDU mutuallyAuthenticate() throws CardException {
|
||||
return secureChannel.mutuallyAuthenticate(apduChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class.
|
||||
*/
|
||||
public ResponseAPDU mutuallyAuthenticate(byte[] data) throws CardException {
|
||||
return secureChannel.mutuallyAuthenticate(apduChannel, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a PAIR APDU. Calls the corresponding method of the SecureChannel class.
|
||||
*/
|
||||
public ResponseAPDU pair(byte p1, byte[] data) throws CardException {
|
||||
return secureChannel.pair(apduChannel, p1, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a UNPAIR APDU. Calls the corresponding method of the SecureChannel class.
|
||||
*/
|
||||
public ResponseAPDU unpair(byte p1) throws CardException {
|
||||
return secureChannel.unpair(apduChannel, p1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 CardException communication error
|
||||
*/
|
||||
public ResponseAPDU getStatus(byte info) throws CardException {
|
||||
CommandAPDU getStatus = secureChannel.protectedCommand(0x80, WalletApplet.INS_GET_STATUS, info, 0, new byte[0]);
|
||||
return secureChannel.transmit(apduChannel, getStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a SET NDEF APDU.
|
||||
*
|
||||
* @param ndef the data field of the APDU
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU setNDEF(byte[] ndef) throws CardException {
|
||||
CommandAPDU setNDEF = secureChannel.protectedCommand(0x80, WalletApplet.INS_SET_NDEF, 0, 0, ndef);
|
||||
return secureChannel.transmit(apduChannel, setNDEF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a GET STATUS APDU to retrieve the APPLICATION STATUS template and reads the byte indicating key initialization
|
||||
* status
|
||||
*
|
||||
* @return whether public key derivation is supported or not
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public boolean getKeyInitializationStatus() throws CardException {
|
||||
ResponseAPDU resp = getStatus(WalletApplet.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 CardException communication error
|
||||
*/
|
||||
public ResponseAPDU verifyPIN(String pin) throws CardException {
|
||||
CommandAPDU verifyPIN = secureChannel.protectedCommand(0x80, WalletApplet.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 pinType the PIN type
|
||||
* @param pin the new PIN
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU changePIN(int pinType, String pin) throws CardException {
|
||||
return changePIN(pinType, pin.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a CHANGE PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
|
||||
* data.
|
||||
*
|
||||
* @param pinType the PIN type
|
||||
* @param pin the new PIN
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU changePIN(int pinType, byte[] pin) throws CardException {
|
||||
CommandAPDU changePIN = secureChannel.protectedCommand(0x80, WalletApplet.INS_CHANGE_PIN, pinType, 0, pin);
|
||||
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 CardException communication error
|
||||
*/
|
||||
public ResponseAPDU unblockPIN(String puk, String newPin) throws CardException {
|
||||
CommandAPDU unblockPIN = secureChannel.protectedCommand(0x80, WalletApplet.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 WalletApplet.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 CardException communication error
|
||||
*/
|
||||
public ResponseAPDU loadKey(PrivateKey aPrivate, byte[] chainCode) throws CardException {
|
||||
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, WalletApplet.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 CardException communication error
|
||||
*/
|
||||
public ResponseAPDU loadKey(KeyPair ecKeyPair) throws CardException {
|
||||
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 WalletApplet.LOAD_KEY_P1_EC or WalletApplet.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 CardException communication error
|
||||
*/
|
||||
public ResponseAPDU loadKey(KeyPair keyPair, boolean omitPublicKey, byte[] chainCode) throws CardException {
|
||||
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, includes the public key and no chain code, meaning that
|
||||
* the card will not be able to do further key derivation. This is needed when the argument is an EC keypair from
|
||||
* the web3j package instead of the regular Java ones. Used by the test which actually submits the transaction to
|
||||
* the network.
|
||||
*
|
||||
* @param ecKeyPair a key pair
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU loadKey(ECKeyPair ecKeyPair) throws CardException {
|
||||
byte[] publicKey = ecKeyPair.getPublicKey().toByteArray();
|
||||
byte[] privateKey = ecKeyPair.getPrivateKey().toByteArray();
|
||||
|
||||
int pubLen = publicKey.length;
|
||||
int pubOff = 0;
|
||||
|
||||
if(publicKey[0] == 0x00) {
|
||||
pubOff++;
|
||||
pubLen--;
|
||||
}
|
||||
|
||||
byte[] ansiPublic = new byte[pubLen + 1];
|
||||
ansiPublic[0] = 0x04;
|
||||
System.arraycopy(publicKey, pubOff, ansiPublic, 1, pubLen);
|
||||
|
||||
return loadKey(ansiPublic, privateKey, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 WalletApplet.LOAD_KEY_P1_EC or
|
||||
* WalletApplet.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 CardException communication error
|
||||
*/
|
||||
public ResponseAPDU loadKey(byte[] publicKey, byte[] privateKey, byte[] chainCode) throws CardException {
|
||||
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++] = WalletApplet.TLV_PUB_KEY;
|
||||
data[off++] = (byte) publicKey.length;
|
||||
System.arraycopy(publicKey, 0, data, off, publicKey.length);
|
||||
off += publicKey.length;
|
||||
}
|
||||
|
||||
data[off++] = WalletApplet.TLV_PRIV_KEY;
|
||||
data[off++] = (byte) privLen;
|
||||
System.arraycopy(privateKey, privOff, data, off, privLen);
|
||||
off += privLen;
|
||||
|
||||
byte p1;
|
||||
|
||||
if (chainCode != null) {
|
||||
p1 = WalletApplet.LOAD_KEY_P1_EXT_EC;
|
||||
data[off++] = (byte) WalletApplet.TLV_CHAIN_CODE;
|
||||
data[off++] = (byte) chainCode.length;
|
||||
System.arraycopy(chainCode, 0, data, off, chainCode.length);
|
||||
} else {
|
||||
p1 = WalletApplet.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 CardException communication error
|
||||
*/
|
||||
public ResponseAPDU loadKey(byte[] data, byte keyType) throws CardException {
|
||||
CommandAPDU loadKey = secureChannel.protectedCommand(0x80, WalletApplet.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 CardException communication error
|
||||
*/
|
||||
public ResponseAPDU generateMnemonic(int cs) throws CardException {
|
||||
CommandAPDU generateMnemonic = secureChannel.protectedCommand(0x80, WalletApplet.INS_GENERATE_MNEMONIC, cs, 0, new byte[0]);
|
||||
return secureChannel.transmit(apduChannel, generateMnemonic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a REMOVE KEY APDU.
|
||||
*
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU removeKey() throws CardException {
|
||||
CommandAPDU removeKey = secureChannel.protectedCommand(0x80, WalletApplet.INS_REMOVE_KEY, 0, 0, new byte[0]);
|
||||
return secureChannel.transmit(apduChannel, removeKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a GENERATE KEY APDU.
|
||||
*
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU generateKey() throws CardException {
|
||||
CommandAPDU generateKey = secureChannel.protectedCommand(0x80, WalletApplet.INS_GENERATE_KEY, 0, 0, new byte[0]);
|
||||
return secureChannel.transmit(apduChannel, generateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a SIGN APDU. This signs a precomputed hash so the input must be exactly 32-bytes long.
|
||||
*
|
||||
* @param data the data to sign
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU sign(byte[] data) throws CardException {
|
||||
CommandAPDU sign = secureChannel.protectedCommand(0x80, WalletApplet.INS_SIGN, 0x00, 0x00, data);
|
||||
return secureChannel.transmit(apduChannel, sign);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The P1 is forced to 0, meaning that the derivation
|
||||
* starts from the master key.
|
||||
*
|
||||
* @param data the raw key path
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU deriveKey(byte[] data) throws CardException {
|
||||
return deriveKey(data, WalletApplet.DERIVE_P1_SOURCE_MASTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The source parameter is used as P1
|
||||
*
|
||||
* @param data the raw key path or a public key
|
||||
* @param source the source to start derivation
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU deriveKey(byte[] data, int source) throws CardException {
|
||||
CommandAPDU deriveKey = secureChannel.protectedCommand(0x80, WalletApplet.INS_DERIVE_KEY, source, 0, 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 CardException communication error
|
||||
*/
|
||||
public ResponseAPDU setPinlessPath(byte [] data) throws CardException {
|
||||
CommandAPDU setPinlessPath = secureChannel.protectedCommand(0x80, WalletApplet.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 CardException communication error
|
||||
*/
|
||||
public ResponseAPDU exportKey(byte keyPathIndex, boolean publicOnly) throws CardException {
|
||||
byte p2 = publicOnly ? WalletApplet.EXPORT_KEY_P2_PUBLIC_ONLY : WalletApplet.EXPORT_KEY_P2_PRIVATE_AND_PUBLIC;
|
||||
CommandAPDU exportKey = secureChannel.protectedCommand(0x80, WalletApplet.INS_EXPORT_KEY, keyPathIndex, p2, new byte[0]);
|
||||
return secureChannel.transmit(apduChannel, exportKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the INIT command to the card.
|
||||
*
|
||||
* @param pin the PIN
|
||||
* @param puk the PUK
|
||||
* @param sharedSecret the shared secret for pairing
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU init(String pin, String puk, byte[] sharedSecret) throws CardException {
|
||||
byte[] initData = Arrays.copyOf(pin.getBytes(), pin.length() + puk.length() + sharedSecret.length);
|
||||
System.arraycopy(puk.getBytes(), 0, initData, pin.length(), puk.length());
|
||||
System.arraycopy(sharedSecret, 0, initData, pin.length() + puk.length(), sharedSecret.length);
|
||||
CommandAPDU init = new CommandAPDU(0x80, WalletApplet.INS_INIT, 0, 0, secureChannel.oneShotEncrypt(initData));
|
||||
return apduChannel.transmit(init);
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ package im.status.wallet;
|
||||
import com.licel.jcardsim.smartcardio.CardSimulator;
|
||||
import com.licel.jcardsim.smartcardio.CardTerminalSimulator;
|
||||
import com.licel.jcardsim.utils.AIDUtil;
|
||||
import im.status.hardwallet.lite.WalletAppletCommandSet;
|
||||
import javacard.framework.AID;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.crypto.ChildNumber;
|
||||
@ -49,8 +50,8 @@ public class WalletAppletTest {
|
||||
private static CardChannel apduChannel;
|
||||
private static CardSimulator simulator;
|
||||
|
||||
private SecureChannelSession secureChannel;
|
||||
private WalletAppletCommandSet cmdSet;
|
||||
private TestSecureChannelSession secureChannel;
|
||||
private TestWalletAppletCommandSet cmdSet;
|
||||
|
||||
private static final boolean USE_SIMULATOR;
|
||||
|
||||
@ -88,18 +89,16 @@ public class WalletAppletTest {
|
||||
WalletAppletCommandSet cmdSet = new WalletAppletCommandSet(apduChannel);
|
||||
byte[] data = cmdSet.select().getData();
|
||||
if (data[0] == WalletApplet.TLV_APPLICATION_INFO_TEMPLATE) return;
|
||||
|
||||
SecureChannelSession secureChannel = new SecureChannelSession(Arrays.copyOfRange(data, 2, data.length));
|
||||
cmdSet.setSecureChannel(secureChannel);
|
||||
assertEquals(0x9000, cmdSet.init("000000", "123456789012", SHARED_SECRET).getSW());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void init() throws CardException {
|
||||
reset();
|
||||
cmdSet = new WalletAppletCommandSet(apduChannel);
|
||||
byte[] keyData = extractPublicKeyFromSelect(cmdSet.select().getData());
|
||||
secureChannel = new SecureChannelSession(keyData);
|
||||
cmdSet = new TestWalletAppletCommandSet(apduChannel);
|
||||
secureChannel = new TestSecureChannelSession();
|
||||
cmdSet.setSecureChannel(secureChannel);
|
||||
WalletAppletCommandSet.checkOK(cmdSet.select());
|
||||
cmdSet.setSecureChannel(secureChannel);
|
||||
cmdSet.autoPair(SHARED_SECRET);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user