mirror of
https://github.com/status-im/status-keycard.git
synced 2025-03-02 22:00:42 +00:00
Merge pull request #26 from status-im/backup-restore
implement backup/restore
This commit is contained in:
commit
2ff8de6e80
@ -325,6 +325,48 @@ command until a new LOAD KEY command is performed.
|
|||||||
Generates and stores keys completely on card. The state of the card after execution is the same as if a LOAD KEY command
|
Generates and stores keys completely on card. The state of the card after execution is the same as if a LOAD KEY command
|
||||||
had been performed.
|
had been performed.
|
||||||
|
|
||||||
|
### DUPLICATE KEY
|
||||||
|
|
||||||
|
* CLA = 0x80
|
||||||
|
* INS = 0xD5
|
||||||
|
* P1 = subcommand
|
||||||
|
* P2 = depends on subcommand
|
||||||
|
* Data = depends on phase
|
||||||
|
* Response SW = 0x9000 on success.
|
||||||
|
* Response Data = depends on subcommand
|
||||||
|
* Preconditions: depends on subcommand
|
||||||
|
|
||||||
|
P1:
|
||||||
|
* 0x00: START DUPLICATE
|
||||||
|
* 0x01: ADD ENTROPY
|
||||||
|
* 0x02: EXPORT DUPLICATE
|
||||||
|
* 0x03: IMPORT DUPLICATE
|
||||||
|
|
||||||
|
#### START DUPLICATE
|
||||||
|
This is the first step to start duplication. Requires an open secure channel and user PIN must be verified. Aborts any
|
||||||
|
on-going duplication session. P2 is the number of entropy pieces to expect in total (including this command). The data
|
||||||
|
contain the first piece of entropy. Returns no data. Must be performed with exactly the same parameters and data on all
|
||||||
|
cards taking part in the duplication.
|
||||||
|
|
||||||
|
#### ADD ENTROPY
|
||||||
|
This command uses the same one-shot secure channel scheme as defined in the INIT command. P2 is 00. Requires an ongoing
|
||||||
|
duplicate session started with the START DUPLICATE subcommand. Must be performed once per device taking part in the
|
||||||
|
duplication process, for a total number of devices equaling the P2 parameter of the START DUPLICATE subcommand (counting
|
||||||
|
the device which sent the START DUPLICATE command as the first device). The data is a random 256-bit number. The same
|
||||||
|
data must be sent to all the cards taking part in the duplication process.
|
||||||
|
|
||||||
|
#### EXPORT DUPLICATE
|
||||||
|
This command must be sent to the card which you wish to duplicate. Requires an open secure channel and authenticated
|
||||||
|
PIN. Works only if a duplication session is active and ADD ENTROPY has been performed the required number of times.
|
||||||
|
Returns the encrypted duplicate of the master key and terminates the duplication session for this card. The format is
|
||||||
|
exactly the same as the one defined in the LOAD KEY (TLV) command with omitted public key. It is however prepended by a
|
||||||
|
16-bytes IV and the entire TLV structure is encrypted.
|
||||||
|
|
||||||
|
#### IMPORT DUPLICATE
|
||||||
|
This command must be sent to all the cards which are a target for duplication. The Data field must contain the output
|
||||||
|
from the EXPORT DUPLICATE command performed on the source card. Returns the key UID. It follows exactly the same rules
|
||||||
|
as the EXPORT DUPLICATE subcommand.
|
||||||
|
|
||||||
### SIGN
|
### SIGN
|
||||||
|
|
||||||
* CLA = 0x80
|
* CLA = 0x80
|
||||||
|
@ -42,7 +42,7 @@ dependencies {
|
|||||||
testCompile('org.web3j:core:2.3.1')
|
testCompile('org.web3j:core:2.3.1')
|
||||||
testCompile('org.bitcoinj:bitcoinj-core:0.14.5')
|
testCompile('org.bitcoinj:bitcoinj-core:0.14.5')
|
||||||
testCompile("org.bouncycastle:bcprov-jdk15on:1.58")
|
testCompile("org.bouncycastle:bcprov-jdk15on:1.58")
|
||||||
testCompile("com.github.status-im:hardwallet-lite-sdk:7e3787f")
|
testCompile("com.github.status-im:hardwallet-lite-sdk:f64cefd")
|
||||||
testCompile("org.junit.jupiter:junit-jupiter-api:5.1.1")
|
testCompile("org.junit.jupiter:junit-jupiter-api:5.1.1")
|
||||||
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.1.1")
|
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.1.1")
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,15 @@ package im.status.wallet;
|
|||||||
import javacard.framework.JCSystem;
|
import javacard.framework.JCSystem;
|
||||||
import javacard.framework.Util;
|
import javacard.framework.Util;
|
||||||
import javacard.security.*;
|
import javacard.security.*;
|
||||||
|
import javacardx.crypto.Cipher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crypto utilities, mostly BIP32 related. The init method must be called during application installation. This class
|
* Crypto utilities, mostly BIP32 related. The init method must be called during application installation. This class
|
||||||
* is not meant to be instantiated.
|
* is not meant to be instantiated.
|
||||||
*/
|
*/
|
||||||
public class Crypto {
|
public class Crypto {
|
||||||
|
final static public short AES_BLOCK_SIZE = 16;
|
||||||
|
|
||||||
final static private short KEY_SECRET_SIZE = 32;
|
final static private short KEY_SECRET_SIZE = 32;
|
||||||
final static private short KEY_DERIVATION_INPUT_SIZE = 37;
|
final static private short KEY_DERIVATION_INPUT_SIZE = 37;
|
||||||
final static private short HMAC_OUT_SIZE = 64;
|
final static private short HMAC_OUT_SIZE = 64;
|
||||||
@ -23,15 +26,18 @@ public class Crypto {
|
|||||||
|
|
||||||
final static private byte[] KEY_BITCOIN_SEED = {'B', 'i', 't', 'c', 'o', 'i', 'n', ' ', 's', 'e', 'e', 'd'};
|
final static private byte[] KEY_BITCOIN_SEED = {'B', 'i', 't', 'c', 'o', 'i', 'n', ' ', 's', 'e', 'e', 'd'};
|
||||||
|
|
||||||
// The below 4 objects can be accessed anywhere from the entire applet
|
// The below 5 objects can be accessed anywhere from the entire applet
|
||||||
RandomData random;
|
RandomData random;
|
||||||
KeyAgreement ecdh;
|
KeyAgreement ecdh;
|
||||||
MessageDigest sha256;
|
MessageDigest sha256;
|
||||||
MessageDigest sha512;
|
MessageDigest sha512;
|
||||||
|
Cipher aesCbcIso9797m2;
|
||||||
|
|
||||||
private Signature hmacSHA512;
|
private Signature hmacSHA512;
|
||||||
private HMACKey hmacKey;
|
private HMACKey hmacKey;
|
||||||
|
|
||||||
|
private AESKey tmpAES256;
|
||||||
|
|
||||||
private byte[] tmp;
|
private byte[] tmp;
|
||||||
|
|
||||||
Crypto() {
|
Crypto() {
|
||||||
@ -39,6 +45,9 @@ public class Crypto {
|
|||||||
sha256 = MessageDigest.getInstance(MessageDigest.ALG_SHA_256, false);
|
sha256 = MessageDigest.getInstance(MessageDigest.ALG_SHA_256, false);
|
||||||
ecdh = KeyAgreement.getInstance(KeyAgreement.ALG_EC_SVDP_DH_PLAIN, false);
|
ecdh = KeyAgreement.getInstance(KeyAgreement.ALG_EC_SVDP_DH_PLAIN, false);
|
||||||
sha512 = MessageDigest.getInstance(MessageDigest.ALG_SHA_512, false);
|
sha512 = MessageDigest.getInstance(MessageDigest.ALG_SHA_512, false);
|
||||||
|
aesCbcIso9797m2 = Cipher.getInstance(Cipher.ALG_AES_CBC_ISO9797_M2,false);
|
||||||
|
|
||||||
|
tmpAES256 = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES_TRANSIENT_DESELECT, KeyBuilder.LENGTH_AES_256, false);
|
||||||
|
|
||||||
short blockSize;
|
short blockSize;
|
||||||
|
|
||||||
@ -54,6 +63,12 @@ public class Crypto {
|
|||||||
tmp = JCSystem.makeTransientByteArray((short) (HMAC_BLOCK_OFFSET + blockSize), JCSystem.CLEAR_ON_RESET);
|
tmp = JCSystem.makeTransientByteArray((short) (HMAC_BLOCK_OFFSET + blockSize), JCSystem.CLEAR_ON_RESET);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public short oneShotAES(byte mode, byte[] src, short sOff, short sLen, byte[] dst, short dOff, byte[] key, short keyOff) {
|
||||||
|
tmpAES256.setKey(key, keyOff);
|
||||||
|
aesCbcIso9797m2.init(tmpAES256, mode, src, sOff, AES_BLOCK_SIZE);
|
||||||
|
return aesCbcIso9797m2.doFinal(src, (short) (sOff + AES_BLOCK_SIZE), sLen, dst, dOff);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derives a private key according to the algorithm defined in BIP32. The BIP32 specifications define some checks
|
* Derives a private key according to the algorithm defined in BIP32. The BIP32 specifications define some checks
|
||||||
* to be performed on the derived keys. In the very unlikely event that these checks fail this key is not considered
|
* to be performed on the derived keys. In the very unlikely event that these checks fail this key is not considered
|
||||||
|
@ -11,7 +11,7 @@ public class SecureChannel {
|
|||||||
public static final short SC_KEY_LENGTH = 256;
|
public static final short SC_KEY_LENGTH = 256;
|
||||||
public static final short SC_SECRET_LENGTH = 32;
|
public static final short SC_SECRET_LENGTH = 32;
|
||||||
public static final short PAIRING_KEY_LENGTH = SC_SECRET_LENGTH + 1;
|
public static final short PAIRING_KEY_LENGTH = SC_SECRET_LENGTH + 1;
|
||||||
public static final short SC_BLOCK_SIZE = 16;
|
public static final short SC_BLOCK_SIZE = Crypto.AES_BLOCK_SIZE;
|
||||||
public static final short SC_OUT_OFFSET = ISO7816.OFFSET_CDATA + (SC_BLOCK_SIZE * 2);
|
public static final short SC_OUT_OFFSET = ISO7816.OFFSET_CDATA + (SC_BLOCK_SIZE * 2);
|
||||||
public static final short SC_COUNTER_MAX = 100;
|
public static final short SC_COUNTER_MAX = 100;
|
||||||
|
|
||||||
@ -28,7 +28,6 @@ public class SecureChannel {
|
|||||||
|
|
||||||
private AESKey scEncKey;
|
private AESKey scEncKey;
|
||||||
private AESKey scMacKey;
|
private AESKey scMacKey;
|
||||||
private Cipher scCipher;
|
|
||||||
private Signature scMac;
|
private Signature scMac;
|
||||||
private KeyPair scKeypair;
|
private KeyPair scKeypair;
|
||||||
private byte[] secret;
|
private byte[] secret;
|
||||||
@ -55,8 +54,6 @@ public class SecureChannel {
|
|||||||
public SecureChannel(byte pairingLimit, Crypto crypto, SECP256k1 secp256k1) {
|
public SecureChannel(byte pairingLimit, Crypto crypto, SECP256k1 secp256k1) {
|
||||||
this.crypto = crypto;
|
this.crypto = crypto;
|
||||||
|
|
||||||
scCipher = Cipher.getInstance(Cipher.ALG_AES_CBC_ISO9797_M2,false);
|
|
||||||
|
|
||||||
scMac = Signature.getInstance(Signature.ALG_AES_MAC_128_NOPAD, false);
|
scMac = Signature.getInstance(Signature.ALG_AES_MAC_128_NOPAD, false);
|
||||||
|
|
||||||
scEncKey = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES_TRANSIENT_DESELECT, KeyBuilder.LENGTH_AES_256, false);
|
scEncKey = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES_TRANSIENT_DESELECT, KeyBuilder.LENGTH_AES_256, false);
|
||||||
@ -88,12 +85,10 @@ public class SecureChannel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypts the content of the APDU by generating an AES key using EC-DH. Only usable in pre-initialization state.
|
* Decrypts the content of the APDU by generating an AES key using EC-DH. Usable only with specific commands.
|
||||||
* @param apduBuffer the APDU buffer
|
* @param apduBuffer the APDU buffer
|
||||||
*/
|
*/
|
||||||
public void oneShotDecrypt(byte[] apduBuffer) {
|
public void oneShotDecrypt(byte[] apduBuffer) {
|
||||||
if (pairingSecret != null) return;
|
|
||||||
|
|
||||||
crypto.ecdh.init(scKeypair.getPrivate());
|
crypto.ecdh.init(scKeypair.getPrivate());
|
||||||
|
|
||||||
short off = (short)(ISO7816.OFFSET_CDATA + 1);
|
short off = (short)(ISO7816.OFFSET_CDATA + 1);
|
||||||
@ -106,10 +101,10 @@ public class SecureChannel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scEncKey.setKey(secret, (short) 0);
|
scEncKey.setKey(secret, (short) 0);
|
||||||
scCipher.init(scEncKey, Cipher.MODE_DECRYPT, apduBuffer, off, SC_BLOCK_SIZE);
|
crypto.aesCbcIso9797m2.init(scEncKey, Cipher.MODE_DECRYPT, apduBuffer, off, SC_BLOCK_SIZE);
|
||||||
off = (short)(off + SC_BLOCK_SIZE);
|
off = (short)(off + SC_BLOCK_SIZE);
|
||||||
|
|
||||||
apduBuffer[ISO7816.OFFSET_LC] = (byte) scCipher.doFinal(apduBuffer, off, (short)((short)(apduBuffer[ISO7816.OFFSET_LC] & 0xff) - off + ISO7816.OFFSET_CDATA), apduBuffer, ISO7816.OFFSET_CDATA);
|
apduBuffer[ISO7816.OFFSET_LC] = (byte) crypto.aesCbcIso9797m2.doFinal(apduBuffer, off, (short)((short)(apduBuffer[ISO7816.OFFSET_LC] & 0xff) - off + ISO7816.OFFSET_CDATA), apduBuffer, ISO7816.OFFSET_CDATA);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -304,9 +299,9 @@ public class SecureChannel {
|
|||||||
ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
|
ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
scCipher.init(scEncKey, Cipher.MODE_DECRYPT, secret, (short) 0, SC_BLOCK_SIZE);
|
crypto.aesCbcIso9797m2.init(scEncKey, Cipher.MODE_DECRYPT, secret, (short) 0, SC_BLOCK_SIZE);
|
||||||
Util.arrayCopyNonAtomic(apduBuffer, ISO7816.OFFSET_CDATA, secret, (short) 0, SC_BLOCK_SIZE);
|
Util.arrayCopyNonAtomic(apduBuffer, ISO7816.OFFSET_CDATA, secret, (short) 0, SC_BLOCK_SIZE);
|
||||||
short len = scCipher.doFinal(apduBuffer, (short)(ISO7816.OFFSET_CDATA + SC_BLOCK_SIZE), (short) (apduLen - SC_BLOCK_SIZE), apduBuffer, ISO7816.OFFSET_CDATA);
|
short len = crypto.aesCbcIso9797m2.doFinal(apduBuffer, (short)(ISO7816.OFFSET_CDATA + SC_BLOCK_SIZE), (short) (apduLen - SC_BLOCK_SIZE), apduBuffer, ISO7816.OFFSET_CDATA);
|
||||||
|
|
||||||
apduBuffer[ISO7816.OFFSET_LC] = (byte) len;
|
apduBuffer[ISO7816.OFFSET_LC] = (byte) len;
|
||||||
|
|
||||||
@ -342,8 +337,8 @@ public class SecureChannel {
|
|||||||
Util.setShort(apduBuffer, (short) (SC_OUT_OFFSET + len), sw);
|
Util.setShort(apduBuffer, (short) (SC_OUT_OFFSET + len), sw);
|
||||||
len += 2;
|
len += 2;
|
||||||
|
|
||||||
scCipher.init(scEncKey, Cipher.MODE_ENCRYPT, secret, (short) 0, SC_BLOCK_SIZE);
|
crypto.aesCbcIso9797m2.init(scEncKey, Cipher.MODE_ENCRYPT, secret, (short) 0, SC_BLOCK_SIZE);
|
||||||
len = scCipher.doFinal(apduBuffer, SC_OUT_OFFSET, len, apduBuffer, (short)(ISO7816.OFFSET_CDATA + SC_BLOCK_SIZE));
|
len = crypto.aesCbcIso9797m2.doFinal(apduBuffer, SC_OUT_OFFSET, len, apduBuffer, (short)(ISO7816.OFFSET_CDATA + SC_BLOCK_SIZE));
|
||||||
|
|
||||||
apduBuffer[0] = (byte) (len + SC_BLOCK_SIZE);
|
apduBuffer[0] = (byte) (len + SC_BLOCK_SIZE);
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package im.status.wallet;
|
|||||||
|
|
||||||
import javacard.framework.*;
|
import javacard.framework.*;
|
||||||
import javacard.security.*;
|
import javacard.security.*;
|
||||||
|
import javacardx.crypto.Cipher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The applet's main class. All incoming commands a processed by this class.
|
* The applet's main class. All incoming commands a processed by this class.
|
||||||
@ -20,6 +21,7 @@ public class WalletApplet extends Applet {
|
|||||||
static final byte INS_GENERATE_MNEMONIC = (byte) 0xD2;
|
static final byte INS_GENERATE_MNEMONIC = (byte) 0xD2;
|
||||||
static final byte INS_REMOVE_KEY = (byte) 0xD3;
|
static final byte INS_REMOVE_KEY = (byte) 0xD3;
|
||||||
static final byte INS_GENERATE_KEY = (byte) 0xD4;
|
static final byte INS_GENERATE_KEY = (byte) 0xD4;
|
||||||
|
static final byte INS_DUPLICATE_KEY = (byte) 0xD5;
|
||||||
static final byte INS_SIGN = (byte) 0xC0;
|
static final byte INS_SIGN = (byte) 0xC0;
|
||||||
static final byte INS_SET_PINLESS_PATH = (byte) 0xC1;
|
static final byte INS_SET_PINLESS_PATH = (byte) 0xC1;
|
||||||
static final byte INS_EXPORT_KEY = (byte) 0xC2;
|
static final byte INS_EXPORT_KEY = (byte) 0xC2;
|
||||||
@ -56,6 +58,11 @@ public class WalletApplet extends Applet {
|
|||||||
static final byte GENERATE_MNEMONIC_P1_CS_MAX = 8;
|
static final byte GENERATE_MNEMONIC_P1_CS_MAX = 8;
|
||||||
static final byte GENERATE_MNEMONIC_TMP_OFF = SecureChannel.SC_OUT_OFFSET + ((((GENERATE_MNEMONIC_P1_CS_MAX * 32) + GENERATE_MNEMONIC_P1_CS_MAX) / 11) * 2);
|
static final byte GENERATE_MNEMONIC_TMP_OFF = SecureChannel.SC_OUT_OFFSET + ((((GENERATE_MNEMONIC_P1_CS_MAX * 32) + GENERATE_MNEMONIC_P1_CS_MAX) / 11) * 2);
|
||||||
|
|
||||||
|
static final byte DUPLICATE_KEY_P1_START = 0x00;
|
||||||
|
static final byte DUPLICATE_KEY_P1_ADD_ENTROPY = 0x01;
|
||||||
|
static final byte DUPLICATE_KEY_P1_EXPORT = 0x02;
|
||||||
|
static final byte DUPLICATE_KEY_P1_IMPORT = 0x03;
|
||||||
|
|
||||||
static final byte EXPORT_KEY_P1_ANY = 0x00;
|
static final byte EXPORT_KEY_P1_ANY = 0x00;
|
||||||
static final byte EXPORT_KEY_P1_HIGH = 0x01;
|
static final byte EXPORT_KEY_P1_HIGH = 0x01;
|
||||||
|
|
||||||
@ -111,6 +118,9 @@ public class WalletApplet extends Applet {
|
|||||||
private Crypto crypto;
|
private Crypto crypto;
|
||||||
private SECP256k1 secp256k1;
|
private SECP256k1 secp256k1;
|
||||||
|
|
||||||
|
private byte[] duplicationEncKey;
|
||||||
|
private short expectedEntropy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked during applet installation. Creates an instance of this class. The installation parameters are passed in
|
* Invoked during applet installation. Creates an instance of this class. The installation parameters are passed in
|
||||||
* the given buffer.
|
* the given buffer.
|
||||||
@ -163,6 +173,9 @@ public class WalletApplet extends Applet {
|
|||||||
signature = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false);
|
signature = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false);
|
||||||
secureChannel = new SecureChannel(PAIRING_MAX_CLIENT_COUNT, crypto, secp256k1);
|
secureChannel = new SecureChannel(PAIRING_MAX_CLIENT_COUNT, crypto, secp256k1);
|
||||||
|
|
||||||
|
duplicationEncKey = new byte[(short)(KeyBuilder.LENGTH_AES_256/8)];
|
||||||
|
expectedEntropy = -1;
|
||||||
|
|
||||||
register(bArray, (short) (bOffset + 1), bArray[bOffset]);
|
register(bArray, (short) (bOffset + 1), bArray[bOffset]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,6 +246,9 @@ public class WalletApplet extends Applet {
|
|||||||
case INS_GENERATE_KEY:
|
case INS_GENERATE_KEY:
|
||||||
generateKey(apdu);
|
generateKey(apdu);
|
||||||
break;
|
break;
|
||||||
|
case INS_DUPLICATE_KEY:
|
||||||
|
duplicateKey(apdu);
|
||||||
|
break;
|
||||||
case INS_SIGN:
|
case INS_SIGN:
|
||||||
sign(apdu);
|
sign(apdu);
|
||||||
break;
|
break;
|
||||||
@ -578,13 +594,10 @@ public class WalletApplet extends Applet {
|
|||||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean newExtended = false;
|
|
||||||
|
|
||||||
switch (apduBuffer[ISO7816.OFFSET_P1]) {
|
switch (apduBuffer[ISO7816.OFFSET_P1]) {
|
||||||
case LOAD_KEY_P1_EXT_EC:
|
|
||||||
newExtended = true;
|
|
||||||
case LOAD_KEY_P1_EC:
|
case LOAD_KEY_P1_EC:
|
||||||
loadKeyPair(apduBuffer, newExtended);
|
case LOAD_KEY_P1_EXT_EC:
|
||||||
|
loadKeyPair(apduBuffer);
|
||||||
break;
|
break;
|
||||||
case LOAD_KEY_P1_SEED:
|
case LOAD_KEY_P1_SEED:
|
||||||
loadSeed(apduBuffer);
|
loadSeed(apduBuffer);
|
||||||
@ -620,14 +633,12 @@ public class WalletApplet extends Applet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called internally by the loadKey method to load a key in the TLV format. The presence of the public key is optional
|
* Called internally by the loadKey method to load a key in the TLV format. The presence of the public key is optional.
|
||||||
* if public key derivation is supported on card, otherwise it is mandatory. The presence of a chain code is indicated
|
* The presence of the chain code determines whether the key is extended or not.
|
||||||
* explicitly through the newExtended argument (which is set depending on the P1 parameter of the command).
|
|
||||||
*
|
*
|
||||||
* @param apduBuffer the APDU buffer
|
* @param apduBuffer the APDU buffer
|
||||||
* @param newExtended whether the key to load contains a chain code or not
|
|
||||||
*/
|
*/
|
||||||
private void loadKeyPair(byte[] apduBuffer, boolean newExtended) {
|
private void loadKeyPair(byte[] apduBuffer) {
|
||||||
short pubOffset = (short)(ISO7816.OFFSET_CDATA + (apduBuffer[(short) (ISO7816.OFFSET_CDATA + 1)] == (byte) 0x81 ? 3 : 2));
|
short pubOffset = (short)(ISO7816.OFFSET_CDATA + (apduBuffer[(short) (ISO7816.OFFSET_CDATA + 1)] == (byte) 0x81 ? 3 : 2));
|
||||||
short privOffset = (short)(pubOffset + apduBuffer[(short)(pubOffset + 1)] + 2);
|
short privOffset = (short)(pubOffset + apduBuffer[(short)(pubOffset + 1)] + 2);
|
||||||
short chainOffset = (short)(privOffset + apduBuffer[(short)(privOffset + 1)] + 2);
|
short chainOffset = (short)(privOffset + apduBuffer[(short)(privOffset + 1)] + 2);
|
||||||
@ -638,14 +649,14 @@ public class WalletApplet extends Applet {
|
|||||||
pubOffset = -1;
|
pubOffset = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!((apduBuffer[ISO7816.OFFSET_CDATA] == TLV_KEY_TEMPLATE) && (apduBuffer[privOffset] == TLV_PRIV_KEY) && (!newExtended || apduBuffer[chainOffset] == TLV_CHAIN_CODE))) {
|
if (!((apduBuffer[ISO7816.OFFSET_CDATA] == TLV_KEY_TEMPLATE) && (apduBuffer[privOffset] == TLV_PRIV_KEY))) {
|
||||||
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||||
}
|
}
|
||||||
|
|
||||||
JCSystem.beginTransaction();
|
JCSystem.beginTransaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isExtended = newExtended;
|
isExtended = (apduBuffer[chainOffset] == TLV_CHAIN_CODE);
|
||||||
|
|
||||||
masterPrivate.setS(apduBuffer, (short) (privOffset + 2), apduBuffer[(short) (privOffset + 1)]);
|
masterPrivate.setS(apduBuffer, (short) (privOffset + 2), apduBuffer[(short) (privOffset + 1)]);
|
||||||
privateKey.setS(apduBuffer, (short) (privOffset + 2), apduBuffer[(short) (privOffset + 1)]);
|
privateKey.setS(apduBuffer, (short) (privOffset + 2), apduBuffer[(short) (privOffset + 1)]);
|
||||||
@ -952,12 +963,117 @@ public class WalletApplet extends Applet {
|
|||||||
generateKeyUIDAndRespond(apdu, apduBuffer);
|
generateKeyUIDAndRespond(apdu, apduBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the DUPLICATE KEY command. The actual processing depends on the subcommand.
|
||||||
|
*
|
||||||
|
* @param apdu the JCRE-owned APDU object.
|
||||||
|
*/
|
||||||
|
private void duplicateKey(APDU apdu) {
|
||||||
|
byte[] apduBuffer = apdu.getBuffer();
|
||||||
|
|
||||||
|
if (apduBuffer[ISO7816.OFFSET_P1] == DUPLICATE_KEY_P1_ADD_ENTROPY) {
|
||||||
|
if (expectedEntropy <= 0) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
secureChannel.oneShotDecrypt(apduBuffer);
|
||||||
|
addEntropy(apduBuffer);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
secureChannel.preprocessAPDU(apduBuffer);
|
||||||
|
|
||||||
|
if (!pin.isValidated()) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(apduBuffer[ISO7816.OFFSET_P1]) {
|
||||||
|
case DUPLICATE_KEY_P1_START:
|
||||||
|
startDuplication(apduBuffer);
|
||||||
|
break;
|
||||||
|
case DUPLICATE_KEY_P1_EXPORT:
|
||||||
|
short len = exportDuplicate(apduBuffer);
|
||||||
|
secureChannel.respond(apdu, len, ISO7816.SW_NO_ERROR);
|
||||||
|
break;
|
||||||
|
case DUPLICATE_KEY_P1_IMPORT:
|
||||||
|
importDuplicate(apduBuffer);
|
||||||
|
generateKeyUIDAndRespond(apdu, apduBuffer);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startDuplication(byte[] apduBuffer) {
|
||||||
|
if (apduBuffer[ISO7816.OFFSET_LC] != (short) duplicationEncKey.length) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
JCSystem.beginTransaction();
|
||||||
|
Util.arrayCopy(apduBuffer, ISO7816.OFFSET_CDATA, duplicationEncKey, (short) 0, (short) duplicationEncKey.length);
|
||||||
|
expectedEntropy = (short) (apduBuffer[ISO7816.OFFSET_P2] - 1);
|
||||||
|
JCSystem.commitTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addEntropy(byte[] apduBuffer) {
|
||||||
|
if (apduBuffer[ISO7816.OFFSET_LC] != (short) duplicationEncKey.length) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||||
|
}
|
||||||
|
|
||||||
|
JCSystem.beginTransaction();
|
||||||
|
for (short i = 0; i < (short) duplicationEncKey.length; i++) {
|
||||||
|
duplicationEncKey[i] ^= apduBuffer[(short) (ISO7816.OFFSET_CDATA + i)];
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedEntropy--;
|
||||||
|
JCSystem.commitTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finalizeDuplicationKey() {
|
||||||
|
if (expectedEntropy != 0) {
|
||||||
|
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedEntropy = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private short exportDuplicate(byte[] apduBuffer) {
|
||||||
|
finalizeDuplicationKey();
|
||||||
|
crypto.random.generateData(apduBuffer, SecureChannel.SC_OUT_OFFSET, Crypto.AES_BLOCK_SIZE);
|
||||||
|
short off = (short) (SecureChannel.SC_OUT_OFFSET + Crypto.AES_BLOCK_SIZE);
|
||||||
|
Util.arrayCopyNonAtomic(apduBuffer, SecureChannel.SC_OUT_OFFSET, apduBuffer, off, Crypto.AES_BLOCK_SIZE);
|
||||||
|
off += Crypto.AES_BLOCK_SIZE;
|
||||||
|
|
||||||
|
apduBuffer[off++] = TLV_KEY_TEMPLATE;
|
||||||
|
short keyTemplateLenOff = off++;
|
||||||
|
|
||||||
|
apduBuffer[off++] = TLV_PRIV_KEY;
|
||||||
|
apduBuffer[off] = (byte) masterPrivate.getS(apduBuffer, (short) (off + 1));
|
||||||
|
apduBuffer[keyTemplateLenOff] = (byte) (apduBuffer[off] + 2);
|
||||||
|
off += (short) (apduBuffer[off] + 1);
|
||||||
|
|
||||||
|
if (isExtended) {
|
||||||
|
apduBuffer[off++] = TLV_CHAIN_CODE;
|
||||||
|
apduBuffer[off++] = CHAIN_CODE_SIZE;
|
||||||
|
Util.arrayCopyNonAtomic(masterChainCode, (short) 0, apduBuffer, off, CHAIN_CODE_SIZE);
|
||||||
|
apduBuffer[keyTemplateLenOff] += (byte) (CHAIN_CODE_SIZE + 2);
|
||||||
|
off += CHAIN_CODE_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (short) (Crypto.AES_BLOCK_SIZE + crypto.oneShotAES(Cipher.MODE_ENCRYPT, apduBuffer, (short) (SecureChannel.SC_OUT_OFFSET + Crypto.AES_BLOCK_SIZE), off, apduBuffer, (short) (SecureChannel.SC_OUT_OFFSET + Crypto.AES_BLOCK_SIZE), duplicationEncKey, (short) 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importDuplicate(byte[] apduBuffer) {
|
||||||
|
finalizeDuplicationKey();
|
||||||
|
short len = crypto.oneShotAES(Cipher.MODE_DECRYPT, apduBuffer, ISO7816.OFFSET_CDATA, (short) (apduBuffer[ISO7816.OFFSET_LC] & 0xff), apduBuffer, ISO7816.OFFSET_CDATA, duplicationEncKey, (short) 0);
|
||||||
|
apduBuffer[ISO7816.OFFSET_LC] = (byte) len;
|
||||||
|
loadKeyPair(apduBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes the SIGN command. Requires a secure channel to open and either the PIN to be verified or the PIN-less key
|
* Processes the SIGN command. Requires a secure channel to open and either the PIN to be verified or the PIN-less key
|
||||||
* path to be the current key path. This command supports signing data using SHA-256 with possible segmentation over
|
* path to be the current key path. This command supports signing a precomputed 32-bytes hash. The signature is
|
||||||
* multiple APDUs as well as signing a precomputed 32-bytes hash. The latter option is the actual use case at the
|
|
||||||
* moment, since Ethereum signatures actually require Keccak-256 hashes, which are not supported by any version of
|
|
||||||
* JavaCard (including 3.0.5 which supports SHA-3 but not Keccak-256 which is slightly different). The signature is
|
|
||||||
* generated using the current keys, so if no keys are loaded the command does not work. The result of the execution
|
* generated using the current keys, so if no keys are loaded the command does not work. The result of the execution
|
||||||
* is not the plain signature, but a TLV object containing the public key which must be used to verify the signature
|
* is not the plain signature, but a TLV object containing the public key which must be used to verify the signature
|
||||||
* and the signature itself. The client should use this to calculate 'v' and format the signature according to the
|
* and the signature itself. The client should use this to calculate 'v' and format the signature according to the
|
||||||
|
@ -1017,6 +1017,105 @@ public class WalletAppletTest {
|
|||||||
assertEquals(0x6985, response.getSW());
|
assertEquals(0x6985, response.getSW());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("DUPLICATE KEY command")
|
||||||
|
void duplicateTest() throws Exception {
|
||||||
|
int secretCount = 5;
|
||||||
|
Random random = new Random();
|
||||||
|
byte[][] secrets = new byte[secretCount][32];
|
||||||
|
for (int i = 0; i < secretCount; i++) {
|
||||||
|
random.nextBytes(secrets[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security condition violation: SecureChannel not open
|
||||||
|
ResponseAPDU response = cmdSet.duplicateKeyStart(secretCount, secrets[0]);
|
||||||
|
assertEquals(0x6985, response.getSW());
|
||||||
|
|
||||||
|
cmdSet.autoOpenSecureChannel();
|
||||||
|
|
||||||
|
// Security condition violation: PIN not verified
|
||||||
|
response = cmdSet.duplicateKeyStart(secretCount, secrets[0]);
|
||||||
|
assertEquals(0x6985, response.getSW());
|
||||||
|
|
||||||
|
response = cmdSet.verifyPIN("000000");
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
response = cmdSet.generateKey();
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
byte[] keyUID = response.getData();
|
||||||
|
|
||||||
|
// Init duplication
|
||||||
|
response = cmdSet.duplicateKeyStart(secretCount, secrets[0]);
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
|
||||||
|
// Adding key entropy must work without secure channel and PIN authentication
|
||||||
|
reset();
|
||||||
|
response = cmdSet.select();
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
|
||||||
|
// Put all except the last piece of entropy
|
||||||
|
for (int i = 1; i < (secretCount - 1); i++) {
|
||||||
|
response = cmdSet.duplicateKeyAddEntropy(secrets[i]);
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdSet.autoOpenSecureChannel();
|
||||||
|
response = cmdSet.verifyPIN("000000");
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
|
||||||
|
// Try to backup before enough entropy has been set
|
||||||
|
response = cmdSet.duplicateKeyExport();
|
||||||
|
assertEquals(0x6985, response.getSW());
|
||||||
|
|
||||||
|
reset();
|
||||||
|
response = cmdSet.select();
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
|
||||||
|
// Put last piece of entropy
|
||||||
|
response = cmdSet.duplicateKeyAddEntropy(secrets[(secretCount - 1)]);
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
|
||||||
|
// Try putting more entropy (failure expected)
|
||||||
|
response = cmdSet.duplicateKeyAddEntropy(secrets[(secretCount - 1)]);
|
||||||
|
assertEquals(0x6985, response.getSW());
|
||||||
|
|
||||||
|
cmdSet.autoOpenSecureChannel();
|
||||||
|
response = cmdSet.verifyPIN("000000");
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
|
||||||
|
// Backup
|
||||||
|
response = cmdSet.duplicateKeyExport();
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
byte[] backup = response.getData();
|
||||||
|
|
||||||
|
// Try to restore the backup (failure expected, session is over)
|
||||||
|
response = cmdSet.duplicateKeyImport(backup);
|
||||||
|
assertEquals(0x6985, response.getSW());
|
||||||
|
|
||||||
|
// Now try to restore the backup and check that the key UID matches, but first change the keys to random ones
|
||||||
|
response = cmdSet.generateKey();
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
|
||||||
|
response = cmdSet.duplicateKeyStart(secretCount, secrets[0]);
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
|
||||||
|
reset();
|
||||||
|
response = cmdSet.select();
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
|
||||||
|
for (int i = 1; i < secretCount; i++) {
|
||||||
|
response = cmdSet.duplicateKeyAddEntropy(secrets[i]);
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdSet.autoOpenSecureChannel();
|
||||||
|
response = cmdSet.verifyPIN("000000");
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
|
||||||
|
response = cmdSet.duplicateKeyImport(backup);
|
||||||
|
assertEquals(0x9000, response.getSW());
|
||||||
|
assertArrayEquals(keyUID, response.getData());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Sign actual Ethereum transaction")
|
@DisplayName("Sign actual Ethereum transaction")
|
||||||
@Tag("manual")
|
@Tag("manual")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user