diff --git a/APPLICATION.MD b/APPLICATION.MD index 3d78a14..3dd2fec 100644 --- a/APPLICATION.MD +++ b/APPLICATION.MD @@ -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 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 * CLA = 0x80 diff --git a/build.gradle b/build.gradle index 183924f..633f862 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +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("com.github.status-im:hardwallet-lite-sdk:f64cefd") testCompile("org.junit.jupiter:junit-jupiter-api:5.1.1") testRuntime("org.junit.jupiter:junit-jupiter-engine:5.1.1") } diff --git a/src/main/java/im/status/wallet/Crypto.java b/src/main/java/im/status/wallet/Crypto.java index 7e2676f..ff15820 100644 --- a/src/main/java/im/status/wallet/Crypto.java +++ b/src/main/java/im/status/wallet/Crypto.java @@ -3,12 +3,15 @@ package im.status.wallet; import javacard.framework.JCSystem; import javacard.framework.Util; import javacard.security.*; +import javacardx.crypto.Cipher; /** * Crypto utilities, mostly BIP32 related. The init method must be called during application installation. This class * is not meant to be instantiated. */ public class Crypto { + final static public short AES_BLOCK_SIZE = 16; + final static private short KEY_SECRET_SIZE = 32; final static private short KEY_DERIVATION_INPUT_SIZE = 37; 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'}; - // 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; KeyAgreement ecdh; MessageDigest sha256; MessageDigest sha512; + Cipher aesCbcIso9797m2; private Signature hmacSHA512; private HMACKey hmacKey; + private AESKey tmpAES256; + private byte[] tmp; Crypto() { @@ -39,6 +45,9 @@ public class Crypto { sha256 = MessageDigest.getInstance(MessageDigest.ALG_SHA_256, false); ecdh = KeyAgreement.getInstance(KeyAgreement.ALG_EC_SVDP_DH_PLAIN, 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; @@ -54,6 +63,12 @@ public class Crypto { 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 * to be performed on the derived keys. In the very unlikely event that these checks fail this key is not considered diff --git a/src/main/java/im/status/wallet/SecureChannel.java b/src/main/java/im/status/wallet/SecureChannel.java index ecc2d06..6a701b9 100644 --- a/src/main/java/im/status/wallet/SecureChannel.java +++ b/src/main/java/im/status/wallet/SecureChannel.java @@ -11,7 +11,7 @@ public class SecureChannel { public static final short SC_KEY_LENGTH = 256; public static final short SC_SECRET_LENGTH = 32; 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_COUNTER_MAX = 100; @@ -28,7 +28,6 @@ public class SecureChannel { private AESKey scEncKey; private AESKey scMacKey; - private Cipher scCipher; private Signature scMac; private KeyPair scKeypair; private byte[] secret; @@ -55,8 +54,6 @@ public class SecureChannel { public SecureChannel(byte pairingLimit, Crypto crypto, SECP256k1 secp256k1) { this.crypto = crypto; - scCipher = Cipher.getInstance(Cipher.ALG_AES_CBC_ISO9797_M2,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); @@ -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 */ public void oneShotDecrypt(byte[] apduBuffer) { - if (pairingSecret != null) return; - crypto.ecdh.init(scKeypair.getPrivate()); short off = (short)(ISO7816.OFFSET_CDATA + 1); @@ -106,10 +101,10 @@ public class SecureChannel { } 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); - 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); } - 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); - 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; @@ -342,8 +337,8 @@ public class SecureChannel { Util.setShort(apduBuffer, (short) (SC_OUT_OFFSET + len), sw); len += 2; - scCipher.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)); + crypto.aesCbcIso9797m2.init(scEncKey, Cipher.MODE_ENCRYPT, secret, (short) 0, 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); diff --git a/src/main/java/im/status/wallet/WalletApplet.java b/src/main/java/im/status/wallet/WalletApplet.java index 57df811..60b7e45 100644 --- a/src/main/java/im/status/wallet/WalletApplet.java +++ b/src/main/java/im/status/wallet/WalletApplet.java @@ -2,6 +2,7 @@ package im.status.wallet; import javacard.framework.*; import javacard.security.*; +import javacardx.crypto.Cipher; /** * 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_REMOVE_KEY = (byte) 0xD3; 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_SET_PINLESS_PATH = (byte) 0xC1; 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_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_HIGH = 0x01; @@ -111,6 +118,9 @@ public class WalletApplet extends Applet { private Crypto crypto; 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 * the given buffer. @@ -163,6 +173,9 @@ public class WalletApplet extends Applet { signature = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false); 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]); } @@ -233,6 +246,9 @@ public class WalletApplet extends Applet { case INS_GENERATE_KEY: generateKey(apdu); break; + case INS_DUPLICATE_KEY: + duplicateKey(apdu); + break; case INS_SIGN: sign(apdu); break; @@ -578,13 +594,10 @@ public class WalletApplet extends Applet { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } - boolean newExtended = false; - switch (apduBuffer[ISO7816.OFFSET_P1]) { - case LOAD_KEY_P1_EXT_EC: - newExtended = true; case LOAD_KEY_P1_EC: - loadKeyPair(apduBuffer, newExtended); + case LOAD_KEY_P1_EXT_EC: + loadKeyPair(apduBuffer); break; case LOAD_KEY_P1_SEED: 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 - * if public key derivation is supported on card, otherwise it is mandatory. The presence of a chain code is indicated - * explicitly through the newExtended argument (which is set depending on the P1 parameter of the command). + * Called internally by the loadKey method to load a key in the TLV format. The presence of the public key is optional. + * The presence of the chain code determines whether the key is extended or not. * * @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 privOffset = (short)(pubOffset + apduBuffer[(short)(pubOffset + 1)] + 2); short chainOffset = (short)(privOffset + apduBuffer[(short)(privOffset + 1)] + 2); @@ -638,14 +649,14 @@ public class WalletApplet extends Applet { 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); } JCSystem.beginTransaction(); try { - isExtended = newExtended; + isExtended = (apduBuffer[chainOffset] == TLV_CHAIN_CODE); masterPrivate.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); } + /** + * 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 - * path to be the current key path. This command supports signing data using SHA-256 with possible segmentation over - * 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 + * path to be the current key path. This command supports signing a precomputed 32-bytes hash. The signature is * 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 * and the signature itself. The client should use this to calculate 'v' and format the signature according to the diff --git a/src/test/java/im/status/wallet/WalletAppletTest.java b/src/test/java/im/status/wallet/WalletAppletTest.java index a3ffa82..23ce89a 100644 --- a/src/test/java/im/status/wallet/WalletAppletTest.java +++ b/src/test/java/im/status/wallet/WalletAppletTest.java @@ -1017,6 +1017,105 @@ public class WalletAppletTest { 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 @DisplayName("Sign actual Ethereum transaction") @Tag("manual")