Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ce0136b1d | ||
|
|
2865d2a08a | ||
|
|
3e5bb577d7 | ||
|
|
e882e39105 | ||
|
|
15a2c43a70 | ||
|
|
205f150705 | ||
|
|
a4ff736d6e |
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user