Compare commits

..
7 Commits
Author SHA1 Message Date
Michele Balistreri 7ce0136b1d auto upgrade to new keys 2019-04-05 16:09:57 +03:00
Michele Balistreri 2865d2a08a use key identifier 1 2019-04-05 13:26:58 +03:00
Michele Balistreri 3e5bb577d7 indicate multiple keys in P2 2019-04-05 13:17:11 +03:00
Michele Balistreri e882e39105 use correct encryption scheme 2019-04-05 12:44:05 +03:00
Michele Balistreri 15a2c43a70 actually save the DEK key 2019-04-05 12:38:05 +03:00
Michele Balistreri 205f150705 add PUT KEY command 2019-04-05 12:22:00 +03:00
Bitgamma a4ff736d6e Keycard v2.2 (#15)
* add methods for the extended SIGN command

* add resetPinlessPath
2019-04-04 10:20:38 +03:00
7 changed files with 239 additions and 13 deletions
+2 -2
View File
@@ -15,7 +15,7 @@ You can import the SDK in your Gradle or Maven project using [Jitpack.io](https:
```groovy
dependencies {
implementation 'com.github.status-im.status-keycard-java:android:2.1.1'
implementation 'com.github.status-im.status-keycard-java:android:2.2.0'
}
```
@@ -23,6 +23,6 @@ dependencies {
```groovy
dependencies {
implementation 'com.github.status-im.status-keycard-java:desktop:2.1.1'
implementation 'com.github.status-im.status-keycard-java:desktop:2.2.0'
}
```
@@ -53,6 +53,11 @@ public class KeycardCommandSet {
static final byte DUPLICATE_KEY_P1_EXPORT = 0x02;
static final byte DUPLICATE_KEY_P1_IMPORT = 0x03;
static final byte SIGN_P1_CURRENT_KEY = 0x00;
static final byte SIGN_P1_DERIVE = 0x01;
static final byte SIGN_P1_DERIVE_AND_MAKE_CURRENT = 0x02;
static final byte SIGN_P1_PINLESS = 0x03;
public static final int GENERATE_MNEMONIC_12_WORDS = 0x04;
public static final int GENERATE_MNEMONIC_15_WORDS = 0x05;
public static final int GENERATE_MNEMONIC_18_WORDS = 0x06;
@@ -533,14 +538,58 @@ public class KeycardCommandSet {
}
/**
* Sends a SIGN APDU. This signs a precomputed hash so the input must be exactly 32-bytes long.
* Sends a SIGN APDU. This signs a precomputed hash that must be exactly 32-bytes long.
*
* @param hash the hash to sign
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse sign(byte[] hash) throws IOException {
return sign(hash, SIGN_P1_CURRENT_KEY);
}
/**
* Sends a SIGN APDU. This signs a precomputed hash that must be exactly 32-bytes long. The key used to sign is given
* as a parameter.
*
* @param hash the hash to sign
* @params path the path of the key to use
* @param makeCurrent ture if the key used to sign should become the current key, false otherwise
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse signWithPath(byte[] hash, String path, boolean makeCurrent) throws IOException {
KeyPath keyPath = new KeyPath(path);
byte[] pathData = keyPath.getData();
byte[] data = Arrays.copyOf(hash, hash.length + pathData.length);
System.arraycopy(pathData, 0, data, hash.length, pathData.length);
return sign(data, keyPath.getSource() | (makeCurrent ? SIGN_P1_DERIVE_AND_MAKE_CURRENT : SIGN_P1_DERIVE));
}
/**
* Sends a SIGN APDU. This signs a precomputed hash that must be exactly 32-bytes long. The pinless path will be used
* to sign. This command is the only variant of SIGN which can also be executed without a Secure Channel.
*
* @param hash the hash to sign
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse signPinless(byte[] hash) throws IOException {
return sign(hash, SIGN_P1_PINLESS);
}
/**
* Sends a SIGN APDU. This signs a precomputed hash so the input must be exactly 32-bytes long, eventually followed by
* a derivation path.
*
* @param p1 the p1 parameter
* @param data the data to sign
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse sign(byte[] data) throws IOException {
APDUCommand sign = secureChannel.protectedCommand(0x80, INS_SIGN, 0x00, 0x00, data);
public APDUResponse sign(byte[] data, int p1) throws IOException {
APDUCommand sign = secureChannel.protectedCommand(0x80, INS_SIGN, p1, 0x00, data);
return secureChannel.transmit(apduChannel, sign);
}
@@ -581,6 +630,33 @@ public class KeycardCommandSet {
return secureChannel.transmit(apduChannel, deriveKey);
}
/**
* Sends a SET PINLESS PATH APDU. The path must be absolute, that is starting from the master key.
* @param path the path. Must be an absolute path (i.e: starting from the master key)
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse setPinlessPath(String path) throws IOException {
KeyPath keyPath = new KeyPath(path);
if (keyPath.getSource() != DERIVE_P1_SOURCE_MASTER) {
throw new IllegalArgumentException("Only absolute paths can be set as PINLESS path");
}
return setPinlessPath(keyPath.getData());
}
/**
* Sends an empty SET PINLESS PATH APDU, resetting it. After this command the card does not have a PINless path until
* a new one is set.
*
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse resetPinlessPath() throws IOException {
return setPinlessPath(new byte[]{});
}
/**
* Sends a SET PINLESS PATH APDU. The data is encrypted and sent as-is.
*
@@ -588,7 +664,7 @@ public class KeycardCommandSet {
* @return the raw card response
* @throws IOException communication error
*/
public APDUResponse setPinlessPath(byte [] data) throws IOException {
public APDUResponse setPinlessPath(byte[] data) throws IOException {
APDUCommand setPinlessPath = secureChannel.protectedCommand(0x80, INS_SET_PINLESS_PATH, 0x00, 0x00, data);
return secureChannel.transmit(apduChannel, setPinlessPath);
}
@@ -121,6 +121,21 @@ public class Crypto {
}
}
public static byte[] ecb3des(byte[] key, byte[] data) {
try {
Cipher cipher = Cipher.getInstance("DESede/ECB/NoPadding", "BC");
SecretKeySpec keyDes = new SecretKeySpec(resizeKey24(key), "DES");
cipher.init(Cipher.ENCRYPT_MODE, keyDes);
return cipher.doFinal(data);
} catch (GeneralSecurityException e) {
throw new RuntimeException("Could not encrypt data", e);
}
}
public static byte[] kcv3des(byte[] key) {
return Arrays.copyOf(ecb3des(key, NullBytes8), 3);
}
/**
* Generates a 3DES MAC for SCP02 communication
*
@@ -24,6 +24,7 @@ public class GlobalPlatformCommandSet {
static final byte INS_DELETE = (byte) 0xE4;
static final byte INS_INSTALL = (byte) 0xE6;
static final byte INS_LOAD = (byte) 0xE8;
static final byte INS_PUT_KEY = (byte) 0xD8;
static final byte SELECT_P1_BY_NAME = (byte) 0x04;
static final byte EXTERNAL_AUTHENTICATE_P1 = (byte) 0x01;
@@ -37,7 +38,10 @@ public class GlobalPlatformCommandSet {
private SCP02Keys cardKeys;
private Session session;
private final byte[] testKey = Hex.decode("404142434445464748494a4b4c4d4e4f");
private final byte[] gpDefaultKey = Hex.decode("404142434445464748494a4b4c4d4e4f");
private final SCP02Keys gpDefaultKeys = new SCP02Keys(gpDefaultKey, gpDefaultKey, gpDefaultKey);
private final byte[] developmentKey = Hex.decode("c212e073ff8b4bbfaff4de8ab655221f");
/**
* Constructs a new command set with the given CardChannel.
@@ -46,7 +50,26 @@ public class GlobalPlatformCommandSet {
*/
public GlobalPlatformCommandSet(CardChannel apduChannel) {
this.apduChannel = apduChannel;
this.cardKeys = new SCP02Keys(testKey, testKey);
setCardKeys(developmentKey);
}
/**
* Sets the given key as all of ENC, MAC and DEK static keys, used to derive session keys.
* @param key the key
*/
public void setCardKeys(byte[] key) {
setCardKeys(key, key, key);
}
/**
* Sets the the ENC, MAC and DEK static keys, used to derive session keys.
*
* @param encKey the ENC key
* @param macKey the MAC key
* @param dekKey the DEK key
*/
public void setCardKeys(byte[] encKey, byte[] macKey, byte[] dekKey) {
this.cardKeys = new SCP02Keys(encKey, macKey, dekKey);;
}
/**
@@ -74,7 +97,12 @@ public class GlobalPlatformCommandSet {
APDUCommand cmd = new APDUCommand(0x80, INS_INITIALIZE_UPDATE, 0, 0, hostChallenge, true);
APDUResponse resp = apduChannel.send(cmd);
if (resp.isOK()) {
this.session = SecureChannel.verifyChallenge(hostChallenge, this.cardKeys, resp);
try {
this.session = SecureChannel.verifyChallenge(hostChallenge, this.cardKeys, resp);
} catch(APDUException e) {
this.session = SecureChannel.verifyChallenge(hostChallenge, gpDefaultKeys, resp);
this.session.markAsUsingFallbackKeys();
}
this.secureChannel = new SecureChannel(this.apduChannel, this.session.getKeys());
}
@@ -103,18 +131,93 @@ public class GlobalPlatformCommandSet {
return this.secureChannel.send(cmd);
}
/**
* Opens an SCP02 secure channel with default keys.
* Convenience method for openSecureChannel with auto key ugprade.
*
* @throws APDUException the card didn't respond 0x9000 to either INITIALIZE UPDATE or EXTERNAL AUTHENTICATE
* @throws IOException communication error
*/
public void openSecureChannel() throws APDUException, IOException {
openSecureChannel(true);
}
/**
* Opens an SCP02 secure channel. If with the current keys the card cryptogram cannot be verified, an attempt is made
* to use the default GlobalPlatform keys instead. This does not require additional commands to the card. In case
* the autoUpgradeKeys is set to true and the default GlobalPlatform keys were used, a PUT KEY command is sent to
* change the keys to the current ones.
*
* @param autoUpgradeKeys upgrade keys if default GP keys are loaded
* @throws APDUException the card didn't respond 0x9000 to either INITIALIZE UPDATE or EXTERNAL AUTHENTICATE
* @throws IOException communication error
*/
public void openSecureChannel(boolean autoUpgradeKeys) throws APDUException, IOException {
SecureRandom random = new SecureRandom();
byte[] hostChallenge = new byte[8];
random.nextBytes(hostChallenge);
initializeUpdate(hostChallenge).checkOK();
externalAuthenticate(hostChallenge).checkOK();
if (this.session.usesFallbackKeys() && autoUpgradeKeys) {
this.putSCP02Keys(this.cardKeys.getEncKeyData(), this.cardKeys.getMacKeyData(), this.cardKeys.getDekKeyData(), 0, 1).checkOK();
}
}
/**
* Sends a PUT KEY APDU to load or replace SCP02 keys. The key is used for all 3 of ENC, MAC and DEK.
*
* @param key the key to load
* @param oldKvn the KVN to replace, 0 to put a new key without replacing
* @param newKvn the KVN of the new keyset
* @return
* @throws IOException
*/
public APDUResponse putSCP02Keys(byte[] key, int oldKvn, int newKvn) throws IOException {
return putSCP02Keys(key, key, key, oldKvn, newKvn);
}
/**
* Sends a PUT KEY APDU to load or replace SCP02 keys. The keys are assumed to be 3DES keys
*
* @param encKey the ENC key to load
* @param macKey the MAC key to load
* @param dekKey the DEK key to load
* @param oldKvn the KVN to replace, 0 to put a new key without replacing
* @param newKvn the KVN of the new keyset
* @return
* @throws IOException
*/
public APDUResponse putSCP02Keys(byte[] encKey, byte[] macKey, byte[] dekKey, int oldKvn, int newKvn) throws IOException {
if (encKey.length != 16 || macKey.length != 16 || dekKey.length != 16){
throw new IllegalArgumentException("All keys must be 16-byte 3DES keys");
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write(newKvn);
writeSCP02Key(bos, encKey);
writeSCP02Key(bos, macKey);
writeSCP02Key(bos, dekKey);
APDUCommand cmd = new APDUCommand(0x84, INS_PUT_KEY, oldKvn, 0x81, bos.toByteArray());
return this.secureChannel.send(cmd);
}
/**
* writes an encrypted key for the PUT KEY command
* @param bos the output stream to write to
* @param key the key to encrypt and write
* @throws IOException if the ByteArrayOutputStream throws it (never)
*/
private void writeSCP02Key(ByteArrayOutputStream bos, byte[] key) throws IOException {
byte[] encrypted = Crypto.ecb3des(session.getKeys().getDekKeyData(), key);
byte[] kcv = Crypto.kcv3des(key);
bos.write(0x80);
bos.write(encrypted.length);
bos.write(encrypted);
bos.write(kcv.length);
bos.write(kcv);
}
/**
@@ -6,16 +6,19 @@ package im.status.keycard.globalplatform;
public class SCP02Keys {
public byte[] encKeyData;
public byte[] macKeyData;
public byte[] dekKeyData;
/**
* Constructor. Takes the ENC and MAC keys.
*
* @param encKeyData encryption key
* @param macKeyData mac key
* @param dekKeyData data encryption key
*/
public SCP02Keys(byte[] encKeyData, byte[] macKeyData) {
public SCP02Keys(byte[] encKeyData, byte[] macKeyData, byte[] dekKeyData) {
this.encKeyData = encKeyData;
this.macKeyData = macKeyData;
this.dekKeyData = dekKeyData;
}
/**
@@ -34,4 +37,13 @@ public class SCP02Keys {
public byte[] getMacKeyData() {
return macKeyData;
}
/**
* The DEK key
*
* @return the DEK key
*/
public byte[] getDekKeyData() {
return dekKeyData;
}
}
@@ -8,7 +8,7 @@ import im.status.keycard.io.APDUResponse;
import im.status.keycard.io.CardChannel;
/**
* An SCP02 Secure Channel. Wraps a CardChannel to allow transparent handling of the scure channel.
* An SCP02 Secure Channel. Wraps a CardChannel to allow transparent handling of the secure channel.
*/
public class SecureChannel {
private CardChannel channel;
@@ -77,8 +77,9 @@ public class SecureChannel {
byte[] sessionEncKey = Crypto.deriveSCP02SessionKey(cardKeys.getEncKeyData(), seq, DERIVATION_PURPOSE_ENC);
byte[] sessionMacKey = Crypto.deriveSCP02SessionKey(cardKeys.getMacKeyData(), seq, DERIVATION_PURPOSE_MAC);
byte[] sessionDekKey = Crypto.deriveSCP02SessionKey(cardKeys.getDekKeyData(), seq, DERIVATION_PURPOSE_DEK);
SCP02Keys sessionKeys = new SCP02Keys(sessionEncKey, sessionMacKey);
SCP02Keys sessionKeys = new SCP02Keys(sessionEncKey, sessionMacKey, sessionDekKey);
boolean verified = Crypto.verifyCryptogram(sessionKeys.getEncKeyData(), hostChallenge, cardChallenge, cardCryptogram);
if (!verified) {
@@ -6,6 +6,7 @@ package im.status.keycard.globalplatform;
public class Session {
private SCP02Keys keys;
private byte[] cardChallenge;
private boolean fallbackKeys;
/**
* Constructs the SCP02 session.
@@ -16,6 +17,7 @@ public class Session {
public Session(SCP02Keys keys, byte[] cardChallenge) {
this.keys = keys;
this.cardChallenge = cardChallenge;
this.fallbackKeys = false;
}
/**
@@ -33,4 +35,21 @@ public class Session {
public byte[] getCardChallenge() {
return cardChallenge;
}
/**
* Marks this session as using a fallback keyset.
*/
public void markAsUsingFallbackKeys() {
fallbackKeys = true;
}
/**
* True if a fallback keyset is being used.
*
* @return true or false
*/
public boolean usesFallbackKeys() {
return fallbackKeys;
}
}