2 Commits
8 changed files with 135 additions and 25 deletions
+11 -2
View File
@@ -5,22 +5,31 @@
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="5">
<list size="9">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="4">
<list size="9">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="javax.validation.constraints.NotNull" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="6" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
</list>
</value>
</option>
Binary file not shown.
@@ -114,7 +114,7 @@ public class CardManager extends Thread implements NfcAdapter.ReaderCallback {
CardChannel ch = new CardChannel(this.isoDep);
switch (requestedAction) {
case ACTION_INSTALL:
case ACTION_INSTALL:
Installer installer = new Installer(ch, this.assets, this.capPath);
installer.start();
break;
@@ -23,6 +23,7 @@ import javax.crypto.spec.SecretKeySpec;
public class Crypto {
public static final byte[] NullBytes8 = new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
public static long PIN_BOUND = 999999L;
public static long PUK_BOUND = 999999999999L;
public static byte[] deriveKey(byte[] cardKey, byte[] seq, byte[] purposeData) {
@@ -7,16 +7,12 @@ import java.io.IOException;
import java.io.InputStream;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import im.status.applet_installer_test.appletinstaller.apducommands.Delete;
import im.status.applet_installer_test.appletinstaller.apducommands.ExternalAuthenticate;
import im.status.applet_installer_test.appletinstaller.apducommands.InitializeUpdate;
import im.status.applet_installer_test.appletinstaller.apducommands.InstallForInstall;
import im.status.applet_installer_test.appletinstaller.apducommands.InstallForLoad;
import im.status.applet_installer_test.appletinstaller.apducommands.Load;
import im.status.applet_installer_test.appletinstaller.apducommands.Select;
import im.status.applet_installer_test.appletinstaller.apducommands.*;
public class Installer {
private Channel plainChannel;
private Channel channel;
private Keys cardKeys;
private AssetManager assets;
@@ -25,6 +21,7 @@ public class Installer {
static final byte[] cardKeyData = HexUtils.hexStringToByteArray("404142434445464748494a4b4c4d4e4f");
public Installer(Channel channel, AssetManager assets, String capPath) {
this.plainChannel = channel;
this.channel = channel;
this.cardKeys = new Keys(cardKeyData, cardKeyData);
this.assets = assets;
@@ -82,27 +79,34 @@ public class Installer {
APDUCommand loadCmd;
while((loadCmd = load.getCommand()) != null) {
this.send("load " + load.getCount() + "/31", loadCmd);
this.send("load " + load.getCount() + "/37", loadCmd);
}
byte[] packageAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574");
byte[] instanceAID = HexUtils.hexStringToByteArray("53746174757357616C6C6574417070");
Secrets secrets = Secrets.generate();
ByteArrayOutputStream params = new ByteArrayOutputStream();
params.write(secrets.getPuk().getBytes());
params.write(secrets.getPairingToken());
InstallForInstall install = new InstallForInstall(packageAID, appletAID, instanceAID, params.toByteArray());
InstallForInstall install = new InstallForInstall(packageAID, appletAID, instanceAID, new byte[0]);
this.send("perform and make selectable", install.getCommand());
Logger.i(String.format("PUK: %s\nPairing password: %s\nPairing token: %s", secrets.getPuk(), secrets.getPairingPassword(), HexUtils.byteArrayToHexString(secrets.getPairingToken())));
installSecrets();
long duration = System.currentTimeMillis() - startTime;
Logger.i(String.format("installation completed in %d seconds", duration / 1000));
}
private void installSecrets() throws NoSuchAlgorithmException, InvalidKeySpecException, APDUException, IOException {
Secrets secrets = Secrets.generate();
Logger.i(String.format("PIN: %s\nPUK: %s\nPairing password: %s\nPairing token: %s", secrets.getPin(), secrets.getPuk(), secrets.getPairingPassword(), HexUtils.byteArrayToHexString(secrets.getPairingToken())));
WalletAppletCommandSet cmdSet = new WalletAppletCommandSet((CardChannel) this.plainChannel);
byte[] ecKey = cmdSet.select().checkOK().getData();
SecureChannelSession secureChannel = new SecureChannelSession(Arrays.copyOfRange(ecKey, 2, ecKey.length));
cmdSet.setSecureChannel(secureChannel);
cmdSet.init(secrets.getPin(), secrets.getPuk(), secrets.getPairingToken()).checkOK();
}
private APDUResponse send(String description, APDUCommand cmd) throws IOException, APDUException {
Logger.d("sending command " + description);
APDUResponse resp = this.channel.send(cmd);
@@ -6,11 +6,13 @@ import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
public class Secrets {
private String pin;
private String puk;
private String pairingPassword;
private byte[] pairingToken;
public Secrets(String puk, String pairingPassword, byte[] pairingToken) {
public Secrets(String pin, String puk, String pairingPassword, byte[] pairingToken) {
this.pin = pin;
this.puk = puk;
this.pairingPassword = pairingPassword;
this.pairingToken = pairingToken;
@@ -20,10 +22,16 @@ public class Secrets {
public static Secrets generate() throws NoSuchAlgorithmException, InvalidKeySpecException {
String pairingPassword = Crypto.randomToken(12);
byte[] pairingToken = Crypto.generatePairingKey(pairingPassword.toCharArray());
long pinNumber = Crypto.randomLong(Crypto.PIN_BOUND);
long pukNumber = Crypto.randomLong(Crypto.PUK_BOUND);
String pin = String.format("%06d", pinNumber);
String puk = String.format("%012d", pukNumber);
return new Secrets(puk, pairingPassword, pairingToken);
return new Secrets(pin, puk, pairingPassword, pairingToken);
}
public String getPin() {
return pin;
}
public String getPuk() {
@@ -57,6 +57,12 @@ public class SecureChannelSession {
* @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 {
random = new SecureRandom();
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
@@ -74,9 +80,7 @@ public class SecureChannelSession {
keyAgreement.doPhase(cardKey, true);
secret = keyAgreement.generateSecret();
open = false;
} catch(Exception e) {
} catch (Exception e) {
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
}
}
@@ -416,6 +420,32 @@ public class SecureChannelSession {
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[SC_BLOCK_SIZE];
random.nextBytes(iv);
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
sessionEncKey = new SecretKeySpec(secret, "AES");
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding");
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.
@@ -11,6 +11,7 @@ import org.spongycastle.util.encoders.Hex;
import java.io.IOException;
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
@@ -18,6 +19,7 @@ import java.security.PrivateKey;
* pre/post processing.
*/
public class WalletAppletCommandSet {
static final byte INS_INIT = (byte) 0xFE;
static final byte INS_GET_STATUS = (byte) 0xF2;
static final byte INS_VERIFY_PIN = (byte) 0x20;
static final byte INS_CHANGE_PIN = (byte) 0x21;
@@ -25,6 +27,7 @@ public class WalletAppletCommandSet {
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_REMOVE_KEY = (byte) 0xD3;
static final byte INS_SIGN = (byte) 0xC0;
static final byte INS_SET_PINLESS_PATH = (byte) 0xC1;
static final byte INS_EXPORT_KEY = (byte) 0xC2;
@@ -174,6 +177,19 @@ public class WalletAppletCommandSet {
return data[data.length - 1] != 0x00;
}
/**
* 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 IOException communication error
*/
public boolean getKeyInitializationStatus() throws IOException {
APDUResponse resp = getStatus(GET_STATUS_P1_APPLICATION);
byte[] data = resp.getData();
return data[data.length - 4] != 0x00;
}
/**
* Sends a VERIFY PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
* data.
@@ -191,12 +207,26 @@ public class WalletAppletCommandSet {
* 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 IOException communication error
*/
public APDUResponse changePIN(String pin) throws IOException {
APDUCommand changePIN = secureChannel.protectedCommand(0x80, INS_CHANGE_PIN, 0, 0, pin.getBytes());
public APDUResponse changePIN(int pinType, String pin) throws IOException {
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 IOException communication error
*/
public APDUResponse changePIN(int pinType, byte[] pin) throws IOException {
APDUCommand changePIN = secureChannel.protectedCommand(0x80, INS_CHANGE_PIN, pinType, 0, pin);
return secureChannel.transmit(apduChannel, changePIN);
}
@@ -362,6 +392,17 @@ public class WalletAppletCommandSet {
return secureChannel.transmit(apduChannel, generateMnemonic);
}
/**
* Sends a REMOVE KEY APDU.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse removeKey() throws IOException {
APDUCommand removeKey = secureChannel.protectedCommand(0x80, INS_REMOVE_KEY, 0, 0, new byte[0]);
return secureChannel.transmit(apduChannel, removeKey);
}
/**
* 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
@@ -438,4 +479,21 @@ public class WalletAppletCommandSet {
APDUCommand exportKey = secureChannel.protectedCommand(0x80, 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 IOException communication error
*/
public APDUResponse init(String pin, String puk, byte[] sharedSecret) throws IOException {
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);
APDUCommand init = new APDUCommand(0x80, INS_INIT, 0, 0, secureChannel.oneShotEncrypt(initData));
return apduChannel.send(init);
}
}