├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── .gitmodules ├── src ├── test │ └── java │ │ └── im │ │ └── status │ │ └── keycard │ │ ├── TestSecureChannelSession.java │ │ ├── Capabilities.java │ │ ├── CapabilityCondition.java │ │ ├── TestKeycardCommandSet.java │ │ └── KeycardTest.java └── main │ └── java │ └── im │ └── status │ └── keycard │ ├── SharedMemory.java │ ├── CashApplet.java │ ├── NDEFApplet.java │ ├── IdentApplet.java │ ├── SECP256k1.java │ ├── Crypto.java │ ├── SecureChannel.java │ └── KeycardApplet.java ├── testwallets ├── wallet1.json └── wallet2.json ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keycard-tech/status-keycard/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ IDEA project files 2 | .idea 3 | *.iml 4 | /.vscode 5 | 6 | # Gradle output 7 | /.gradle 8 | /build 9 | /bin 10 | /gradle.properties 11 | buildSrc/build 12 | buildSrc/.gradle -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "sdks"] 2 | path = sdks 3 | url = https://github.com/martinpaljak/oracle_javacard_sdks 4 | [submodule "jcardsim"] 5 | path = jcardsim 6 | url = https://github.com/status-im/jcardsim 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/test/java/im/status/keycard/TestSecureChannelSession.java: -------------------------------------------------------------------------------- 1 | package im.status.keycard; 2 | 3 | import im.status.keycard.applet.SecureChannelSession; 4 | 5 | public class TestSecureChannelSession extends SecureChannelSession { 6 | public void setOpen() { 7 | super.setOpen(); 8 | } 9 | 10 | public byte getPairingIndex() { 11 | return getPairing().getPairingIndex(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /testwallets/wallet1.json: -------------------------------------------------------------------------------- 1 | {"address":"78d815fc80eee49c1b81a86829c001a02b9fe713","id":"f2a99788-391e-4d7f-8f09-5099663c5610","version":3,"crypto":{"cipher":"aes-128-ctr","ciphertext":"4923648fef859af55741c4c8884c5d4b140c2596d98e4453bb5be72164b6f4cd","cipherparams":{"iv":"b61b25508c0855defbd144b1d85d902d"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"1abd3eed195a899835053c8b665fc7d2e71de4eea63a01416a7047221a909942"},"mac":"2acbaa7c0f3949794c82f34e64da3ff8a8a923b9ec72ff911db4de25f714631e"}} -------------------------------------------------------------------------------- /testwallets/wallet2.json: -------------------------------------------------------------------------------- 1 | {"address":"4faa9795a864f6f3f59dc3c358ed5358025255a3","id":"61cb9458-e846-4ae1-bc5d-b1a00ee0dbcf","version":3,"crypto":{"cipher":"aes-128-ctr","ciphertext":"71caf721a2cd4e914227a213eae35344783c2f6637869ebb1f6e31176902c828","cipherparams":{"iv":"21169cb7b1a259a1386dcccdded17ee6"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"c6c0de87dbdf7e14a56cf71c0b3aef51d5964f1474f029a12804d54cac7e82d7"},"mac":"78f2d1d8f099f37aa612a4877415ad7ee1bd4452f5c785796f890c917d2f6bfc"}} -------------------------------------------------------------------------------- /src/test/java/im/status/keycard/Capabilities.java: -------------------------------------------------------------------------------- 1 | package im.status.keycard; 2 | 3 | import org.junit.jupiter.api.extension.ExtendWith; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Target({ ElementType.TYPE, ElementType.METHOD }) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @ExtendWith(CapabilityCondition.class) 13 | public @interface Capabilities { 14 | String[] value(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/im/status/keycard/SharedMemory.java: -------------------------------------------------------------------------------- 1 | package im.status.keycard; 2 | 3 | import javacard.security.*; 4 | 5 | /** 6 | * Keep references to data structures shared across applet instances of this package. 7 | */ 8 | class SharedMemory { 9 | static final short CERT_LEN = 98; 10 | 11 | /** The NDEF data file. Read through the NDEFApplet. **/ 12 | static final byte[] ndefDataFile = new byte[NDEFApplet.NDEF_MAX_SIZE]; 13 | 14 | /** The Cash data file. Read through the CashApplet. **/ 15 | static final byte[] cashDataFile = new byte[KeycardApplet.MAX_DATA_LENGTH + 1]; 16 | 17 | /** The identification private key **/ 18 | static ECPrivateKey idPrivate = null; 19 | 20 | /** The certificate. It is the concatenation of: compressed id public key, CA signature. 21 | * The signature is in the format r,s,v where v allows recovering the signer public key. */ 22 | static final byte[] idCert = new byte[(short)(CERT_LEN + 1)]; 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/im/status/keycard/CapabilityCondition.java: -------------------------------------------------------------------------------- 1 | package im.status.keycard; 2 | 3 | import org.junit.jupiter.api.extension.ConditionEvaluationResult; 4 | import org.junit.jupiter.api.extension.ExecutionCondition; 5 | import org.junit.jupiter.api.extension.ExtensionContext; 6 | 7 | import java.lang.reflect.AnnotatedElement; 8 | import java.util.HashSet; 9 | import java.util.Optional; 10 | 11 | import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; 12 | import static org.junit.jupiter.api.extension.ConditionEvaluationResult.disabled; 13 | import static org.junit.jupiter.api.extension.ConditionEvaluationResult.enabled; 14 | 15 | public class CapabilityCondition implements ExecutionCondition { 16 | private static final ConditionEvaluationResult ENABLED_BY_DEFAULT = enabled("@Capabilities is not present"); 17 | private static final ConditionEvaluationResult ENABLED = enabled("All capability requirements are satisfied"); 18 | static HashSet availableCapabilities; 19 | 20 | /** 21 | * Containers/tests are disabled if {@code @Disabled} is present on the test 22 | * class or method. 23 | */ 24 | @Override 25 | public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { 26 | Optional element = context.getElement(); 27 | Optional capsAnnotation = findAnnotation(element, Capabilities.class); 28 | 29 | if (capsAnnotation.isPresent()) { 30 | for (String c : capsAnnotation.get().value()) { 31 | if (!availableCapabilities.contains(c)) { 32 | return disabled("The " + c + " capability is not available on the tested target"); 33 | } 34 | } 35 | 36 | return ENABLED; 37 | } 38 | 39 | return ENABLED_BY_DEFAULT; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/test/java/im/status/keycard/TestKeycardCommandSet.java: -------------------------------------------------------------------------------- 1 | package im.status.keycard; 2 | 3 | import im.status.keycard.applet.ApplicationStatus; 4 | import im.status.keycard.applet.KeycardCommandSet; 5 | import im.status.keycard.io.APDUCommand; 6 | import im.status.keycard.io.APDUResponse; 7 | import im.status.keycard.io.CardChannel; 8 | import org.web3j.crypto.ECKeyPair; 9 | 10 | import java.io.IOException; 11 | import java.security.PrivateKey; 12 | import java.security.interfaces.ECPrivateKey; 13 | 14 | 15 | public class TestKeycardCommandSet extends KeycardCommandSet { 16 | public TestKeycardCommandSet(CardChannel apduChannel) { 17 | super(apduChannel); 18 | } 19 | 20 | public void setSecureChannel(TestSecureChannelSession secureChannel) { 21 | super.setSecureChannel(secureChannel); 22 | } 23 | 24 | /** 25 | * Sends a LOAD KEY APDU. The key is sent in TLV format, includes the public key and no chain code, meaning that 26 | * the card will not be able to do further key derivation. This is needed when the argument is an EC keypair from 27 | * the web3j package instead of the regular Java ones. Used by the test which actually submits the transaction to 28 | * the network. 29 | * 30 | * @param ecKeyPair a key pair 31 | * @return the raw card response 32 | * @throws IOException communication error 33 | */ 34 | public APDUResponse loadKey(ECKeyPair ecKeyPair) throws IOException { 35 | byte[] publicKey = ecKeyPair.getPublicKey().toByteArray(); 36 | byte[] privateKey = ecKeyPair.getPrivateKey().toByteArray(); 37 | 38 | int pubLen = publicKey.length; 39 | int pubOff = 0; 40 | 41 | if(publicKey[0] == 0x00) { 42 | pubOff++; 43 | pubLen--; 44 | } 45 | 46 | byte[] ansiPublic = new byte[pubLen + 1]; 47 | ansiPublic[0] = 0x04; 48 | System.arraycopy(publicKey, pubOff, ansiPublic, 1, pubLen); 49 | 50 | return loadKey(ansiPublic, privateKey, null); 51 | } 52 | 53 | /** 54 | * Sends a LOAD KEY APDU. The given private key and chain code are formatted as a raw binary seed and the P1 of 55 | * the command is set to LOAD_KEY_P1_SEED (0x03). This works on cards which support public key derivation. 56 | * The loaded keyset is extended and support further key derivation. 57 | * 58 | * @param aPrivate a private key 59 | * @param chainCode the chain code 60 | * @return the raw card response 61 | * @throws IOException communication error 62 | */ 63 | public APDUResponse loadKey(PrivateKey aPrivate, byte[] chainCode) throws IOException { 64 | byte[] privateKey = ((ECPrivateKey) aPrivate).getS().toByteArray(); 65 | 66 | int privLen = privateKey.length; 67 | int privOff = 0; 68 | 69 | if(privateKey[0] == 0x00) { 70 | privOff++; 71 | privLen--; 72 | } 73 | 74 | byte[] data = new byte[chainCode.length + privLen]; 75 | System.arraycopy(privateKey, privOff, data, 0, privLen); 76 | System.arraycopy(chainCode, 0, data, privLen, chainCode.length); 77 | 78 | return loadKey(data, LOAD_KEY_P1_SEED); 79 | } 80 | 81 | /** 82 | * Sends a GET STATUS APDU to retrieve the APPLICATION STATUS template and reads the byte indicating key initialization 83 | * status 84 | * 85 | * @return whether the master key is present or not 86 | * @throws IOException communication error 87 | */ 88 | public boolean getKeyInitializationStatus() throws IOException { 89 | APDUResponse resp = getStatus(GET_STATUS_P1_APPLICATION); 90 | return new ApplicationStatus(resp.getData()).hasMasterKey(); 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is Keycard? 2 | 3 | Keycard is an implementation of a BIP-32 HD wallet running on Javacard 3.0.4+ (see implementation notes) 4 | 5 | It supports among others 6 | - key generation, derivation and signing 7 | - exporting keys defined in the context of EIP-1581 https://eips.ethereum.org/EIPS/eip-1581 8 | - setting up a NFC NDEF tag 9 | 10 | Communication with the Keycard happens through a simple APDU interface, together with a Secure Channel guaranteeing confidentiality, authentication and integrity of all commands. It supports both NFC and ISO7816 physical interfaces, meaning that it is compatible with any Android phone equipped with NFC, and all USB Smartcard readers. 11 | 12 | The most obvious case for integration of Keycard is crypto wallets (ETH, BTC, etc), however it can be used in other systems where a BIP-32 key tree is used and/or you perform authentication/identification. 13 | 14 | # Where to start? 15 | 16 | A good place to start is our [documentation site](https://keycard.tech/docs/) 17 | 18 | Keycard is a public good — contributions are welcome and highly encouraged! 19 | 20 | You can also join the discussion about this project on our [discord channel](https://discord.gg/uJAXk7jFhZ) 21 | 22 | Keycard is at the center of several projects, check the [ecosystem of projects](https://github.com/keycard-tech/keycard-ecosystem-projects/) or [good first issues](https://github.com/orgs/keycard-tech/projects/1/views/2?filterQuery=good+first+issue) 23 | 24 | Should you wish to work on an issue, please claim it first by commenting on the GitHub issue that you want to work on it. This is to prevent duplicated efforts from contributors on the same issue. 25 | 26 | If you just want to use the Keycard as your hardware wallet there are currently three apps supporting it 27 | 28 | 1. Status [[Android](https://play.google.com/store/apps/details?id=im.status.ethereum)][[iOS](https://apps.apple.com/us/app/status-private-communication/id1178893006)] 29 | 2. WallETH [[Android](https://play.google.com/store/apps/details?id=org.walleth)] 30 | 3. Enno Walet https://ennowallet.com/ 31 | 32 | # How to build the project? 33 | 34 | **YOU NEED OPENJDK11 TO BUILD THIS PROJECT** 35 | 36 | make sure your `JAVA_HOME` environment variable points to a working OpenJDK 11 installation. This is a prerequisite for all of the following steps. 37 | 38 | ## Get the source 39 | 40 | Clone this repository with `git clone --recurse-submodules https://github.com/keycard-tech/status-keycard` 41 | 42 | ## Compilation 43 | 44 | Run `./gradlew convertJavacard` 45 | 46 | ## Installation 47 | 48 | Make sure the card you are trying to use is using the T=1 protocol and SCP02 with either the GlobalPlatform default keys (404142434445464748494a4b4c4d4e4f) or the Keycard development cards key (c212e073ff8b4bbfaff4de8ab655221f), otherwise this installation method will not work and you will need to use an external tool like [GlobalPlatformPro](https://github.com/martinpaljak/GlobalPlatformPro). 49 | 50 | 1. Disconnect all card reader terminals from the system, except the one with the card where you want to install the applet 51 | 2. Run `./gradlew install` 52 | 53 | ## Testing 54 | 55 | Run `./gradlew test` 56 | 57 | if you want to test using the simulator instead of a real card, create a file named `gradle.properties` with the content: 58 | 59 | ```im.status.keycard.test.target=simulator``` 60 | 61 | before running the tests. 62 | 63 | # What kind of smartcards can I use? 64 | 65 | * The applet requires JavaCard 3.0.5 or later. 66 | * The class byte of the APDU is not checked since there are no conflicting INS code. 67 | * The GlobalPlatform ISD keys are set to 404142434445464748494a4b4c4d4e4f or c212e073ff8b4bbfaff4de8ab655221f. 68 | 69 | The algorithms the card must support are at least: 70 | * Cipher.ALG_AES_BLOCK_128_CBC_NOPAD 71 | * Cipher.ALG_AES_CBC_ISO9797_M2 72 | * KeyAgreement.ALG_EC_SVDP_DH_PLAIN 73 | * KeyAgreement.ALG_EC_SVDP_DH_PLAIN_XY 74 | * KeyPair.ALG_EC_FP (generation of 256-bit keys) 75 | * MessageDigest.ALG_SHA_256 76 | * MessageDigest.ALG_SHA_512 77 | * RandomData.ALG_SECURE_RANDOM 78 | * Signature.ALG_AES_MAC_128_NOPAD 79 | * Signature.ALG_ECDSA_SHA_256 80 | 81 | Best performance is achieved if the card supports: 82 | * Signature.ALG_HMAC_SHA_512 83 | 84 | # Other related repositories 85 | 86 | Java SDK for Android and Desktop https://github.com/keycard-tech/status-keycard-java 87 | 88 | Swift SDK for iOS13 and above https://github.com/keycard-tech/Keycard.swift 89 | 90 | Keycard CLI for Desktop https://github.com/keycard-tech/keycard-cli 91 | -------------------------------------------------------------------------------- /src/main/java/im/status/keycard/CashApplet.java: -------------------------------------------------------------------------------- 1 | package im.status.keycard; 2 | 3 | import javacard.framework.*; 4 | import javacard.security.*; 5 | 6 | public class CashApplet extends Applet { 7 | private static final short SIGN_OUT_OFF = ISO7816.OFFSET_CDATA + MessageDigest.LENGTH_SHA_256; 8 | private static final byte TLV_PUB_DATA = (byte) 0x82; 9 | 10 | private KeyPair keypair; 11 | private ECPublicKey publicKey; 12 | private ECPrivateKey privateKey; 13 | 14 | private Crypto crypto; 15 | 16 | private Signature signature; 17 | 18 | /** 19 | * Invoked during applet installation. Creates an instance of this class. The installation parameters are passed in 20 | * the given buffer. 21 | * 22 | * @param bArray installation parameters buffer 23 | * @param bOffset offset where the installation parameters begin 24 | * @param bLength length of the installation parameters 25 | */ 26 | public static void install(byte[] bArray, short bOffset, byte bLength) { 27 | new CashApplet(bArray, bOffset, bLength); 28 | } 29 | 30 | /** 31 | * Application constructor. All memory allocation is done here. The reason for this is two-fold: first the card might 32 | * not have Garbage Collection so dynamic allocation will eventually eat all memory. The second reason is to be sure 33 | * that if the application installs successfully, there is no risk of running out of memory because of other applets 34 | * allocating memory. The constructor also registers the applet with the JCRE so that it becomes selectable. 35 | * 36 | * @param bArray installation parameters buffer 37 | * @param bOffset offset where the installation parameters begin 38 | * @param bLength length of the installation parameters 39 | */ 40 | public CashApplet(byte[] bArray, short bOffset, byte bLength) { 41 | crypto = new Crypto(); 42 | 43 | keypair = new KeyPair(KeyPair.ALG_EC_FP, SECP256k1.SECP256K1_KEY_SIZE); 44 | publicKey = (ECPublicKey) keypair.getPublic(); 45 | privateKey = (ECPrivateKey) keypair.getPrivate(); 46 | SECP256k1.setCurveParameters(publicKey); 47 | SECP256k1.setCurveParameters(privateKey); 48 | keypair.genKeyPair(); 49 | 50 | signature = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false); 51 | 52 | short c9Off = (short)(bOffset + bArray[bOffset] + 1); // Skip AID 53 | c9Off += (short)(bArray[c9Off] + 1); // Skip Privileges and parameter length 54 | 55 | short dataLen = Util.makeShort((byte) 0x00, bArray[c9Off]); 56 | if (dataLen > 0) { 57 | Util.arrayCopyNonAtomic(bArray, c9Off, SharedMemory.cashDataFile, (short) 0, (short)(dataLen + 1)); 58 | } 59 | 60 | register(bArray, (short) (bOffset + 1), bArray[bOffset]); 61 | } 62 | 63 | public void process(APDU apdu) throws ISOException { 64 | apdu.setIncomingAndReceive(); 65 | 66 | // Since selection can happen not only by a SELECT command, we check for that separately. 67 | if (selectingApplet()) { 68 | selectApplet(apdu); 69 | return; 70 | } 71 | 72 | byte[] apduBuffer = apdu.getBuffer(); 73 | 74 | try { 75 | switch (apduBuffer[ISO7816.OFFSET_INS]) { 76 | case KeycardApplet.INS_SIGN: 77 | sign(apdu); 78 | break; 79 | case IdentApplet.INS_IDENTIFY_CARD: 80 | IdentApplet.identifyCard(apdu, null, signature); 81 | break; 82 | default: 83 | ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); 84 | break; 85 | } 86 | } catch (CryptoException ce) { 87 | ISOException.throwIt((short)(ISO7816.SW_UNKNOWN | ce.getReason())); 88 | } catch (Exception e) { 89 | ISOException.throwIt(ISO7816.SW_UNKNOWN); 90 | } 91 | } 92 | 93 | private void selectApplet(APDU apdu) { 94 | byte[] apduBuffer = apdu.getBuffer(); 95 | 96 | short off = 0; 97 | 98 | apduBuffer[off++] = KeycardApplet.TLV_APPLICATION_INFO_TEMPLATE; 99 | short lenoff = off++; 100 | 101 | apduBuffer[off++] = KeycardApplet.TLV_PUB_KEY; 102 | short keyLength = publicKey.getW(apduBuffer, (short) (off + 1)); 103 | apduBuffer[off++] = (byte) keyLength; 104 | off += keyLength; 105 | 106 | apduBuffer[off++] = KeycardApplet.TLV_INT; 107 | apduBuffer[off++] = 2; 108 | Util.setShort(apduBuffer, off, KeycardApplet.APPLICATION_VERSION); 109 | off += 2; 110 | 111 | apduBuffer[off++] = TLV_PUB_DATA; 112 | apduBuffer[off++] = SharedMemory.cashDataFile[0]; 113 | Util.arrayCopyNonAtomic(SharedMemory.cashDataFile, (short) 1, apduBuffer, off, SharedMemory.cashDataFile[0]); 114 | off += SharedMemory.cashDataFile[0]; 115 | 116 | apduBuffer[lenoff] = (byte)(off - lenoff - 1); 117 | apdu.setOutgoingAndSend((short) 0, off); 118 | } 119 | 120 | private void sign(APDU apdu) { 121 | byte[] apduBuffer = apdu.getBuffer(); 122 | 123 | apduBuffer[SIGN_OUT_OFF] = KeycardApplet.TLV_SIGNATURE_TEMPLATE; 124 | apduBuffer[(short) (SIGN_OUT_OFF + 3)] = KeycardApplet.TLV_PUB_KEY; 125 | short outLen = apduBuffer[(short) (SIGN_OUT_OFF + 4)] = Crypto.KEY_PUB_SIZE; 126 | 127 | publicKey.getW(apduBuffer, (short) (SIGN_OUT_OFF + 5)); 128 | 129 | outLen += 5; 130 | short sigOff = (short) (SIGN_OUT_OFF + outLen); 131 | 132 | signature.init(privateKey, Signature.MODE_SIGN); 133 | outLen += signature.signPreComputedHash(apduBuffer, ISO7816.OFFSET_CDATA, MessageDigest.LENGTH_SHA_256, apduBuffer, sigOff); 134 | outLen += crypto.fixS(apduBuffer, sigOff); 135 | 136 | apduBuffer[(short) (SIGN_OUT_OFF + 1)] = (byte) 0x81; 137 | apduBuffer[(short) (SIGN_OUT_OFF + 2)] = (byte) (outLen - 3); 138 | 139 | apdu.setOutgoingAndSend(SIGN_OUT_OFF, outLen); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/im/status/keycard/NDEFApplet.java: -------------------------------------------------------------------------------- 1 | package im.status.keycard; 2 | 3 | import javacard.framework.*; 4 | 5 | /** 6 | * The applet's main class. All incoming commands a processed by this class. 7 | */ 8 | public class NDEFApplet extends Applet { 9 | protected static final short NDEF_MAX_SIZE = (short) 512; 10 | private static final byte INS_READ_BINARY = (byte) 0xb0; 11 | 12 | private static final short FILEID_NONE = (short) 0xffff; 13 | private static final short FILEID_NDEF_CAPS = (short) 0xe103; 14 | private static final short FILEID_NDEF_DATA = (short) 0xe104; 15 | 16 | private static final byte SELECT_P1_BY_FILEID = (byte) 0x00; 17 | private static final byte SELECT_P2_FIRST_OR_ONLY = (byte) 0x0c; 18 | 19 | private static final short NDEF_READ_SIZE = (short) 255; 20 | 21 | private static final byte[] NDEF_CAPS_FILE = { 22 | (byte) 0x00, (byte) 0x0f, (byte) 0x20, (byte) 0x00, (byte) 0xff, (byte) 0x00, (byte) 0x01, (byte) 0x04, 23 | (byte) 0x06, (byte) 0xe1, (byte) 0x04, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0xff 24 | }; 25 | 26 | private short selectedFile; 27 | 28 | /** 29 | * Invoked during applet installation. Creates an instance of this class. The installation parameters are passed in 30 | * the given buffer. 31 | * 32 | * @param bArray installation parameters buffer 33 | * @param bOffset offset where the installation parameters begin 34 | * @param bLength length of the installation parameters 35 | */ 36 | public static void install(byte[] bArray, short bOffset, byte bLength) { 37 | new NDEFApplet(bArray, bOffset, bLength); 38 | } 39 | 40 | /** 41 | * Application constructor. All memory allocation is done here. The reason for this is two-fold: first the card might 42 | * not have Garbage Collection so dynamic allocation will eventually eat all memory. The second reason is to be sure 43 | * that if the application installs successfully, there is no risk of running out of memory because of other applets 44 | * allocating memory. The constructor also registers the applet with the JCRE so that it becomes selectable. 45 | * 46 | * @param bArray installation parameters buffer 47 | * @param bOffset offset where the installation parameters begin 48 | * @param bLength length of the installation parameters 49 | */ 50 | public NDEFApplet(byte[] bArray, short bOffset, byte bLength) { 51 | short c9Off = (short)(bOffset + bArray[bOffset] + 1); // Skip AID 52 | c9Off += (short)(bArray[c9Off] + 1); // Skip Privileges and parameter length 53 | 54 | short dataLen = Util.makeShort((byte) 0x00, bArray[c9Off]); 55 | c9Off++; 56 | 57 | if ((dataLen > 2) && ((short)(dataLen - 2) == Util.makeShort(bArray[(short)(c9Off)], bArray[(short)(c9Off + 1)]))) { 58 | Util.arrayCopyNonAtomic(bArray, c9Off, SharedMemory.ndefDataFile, (short) 0, dataLen); 59 | } 60 | 61 | register(bArray, (short) (bOffset + 1), bArray[bOffset]); 62 | } 63 | 64 | /** 65 | * This method is called on every incoming APDU. This method is just a dispatcher which invokes the correct method 66 | * depending on the INS of the APDU. 67 | * 68 | * @param apdu the JCRE-owned APDU object. 69 | * @throws ISOException any processing error 70 | */ 71 | public void process(APDU apdu) throws ISOException { 72 | if (selectingApplet()) { 73 | selectedFile = FILEID_NONE; 74 | return; 75 | } 76 | 77 | byte[] apduBuffer = apdu.getBuffer(); 78 | 79 | switch (apduBuffer[ISO7816.OFFSET_INS]) { 80 | case ISO7816.INS_SELECT: 81 | processSelect(apdu); 82 | break; 83 | case INS_READ_BINARY: 84 | processReadBinary(apdu); 85 | break; 86 | default: 87 | ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); 88 | break; 89 | } 90 | } 91 | 92 | private void processSelect(APDU apdu) { 93 | byte[] apduBuffer = apdu.getBuffer(); 94 | apdu.setIncomingAndReceive(); 95 | 96 | if(apduBuffer[ISO7816.OFFSET_P1] != SELECT_P1_BY_FILEID || apduBuffer[ISO7816.OFFSET_P2] != SELECT_P2_FIRST_OR_ONLY) { 97 | ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); 98 | } else if (apduBuffer[ISO7816.OFFSET_LC] != 2) { 99 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 100 | } 101 | 102 | short fid = Util.getShort(apduBuffer, ISO7816.OFFSET_CDATA); 103 | 104 | switch(fid) { 105 | case FILEID_NDEF_CAPS: 106 | case FILEID_NDEF_DATA: 107 | selectedFile = fid; 108 | break; 109 | default: 110 | ISOException.throwIt(ISO7816.SW_FILE_NOT_FOUND); 111 | break; 112 | } 113 | } 114 | 115 | private void processReadBinary(APDU apdu) { 116 | byte[] apduBuffer = apdu.getBuffer(); 117 | 118 | byte[] data; 119 | short dataLen; 120 | 121 | switch(selectedFile) { 122 | case FILEID_NDEF_CAPS: 123 | data = NDEF_CAPS_FILE; 124 | dataLen = (short) NDEF_CAPS_FILE.length; 125 | break; 126 | case FILEID_NDEF_DATA: 127 | data = SharedMemory.ndefDataFile; 128 | dataLen = (short) (Util.makeShort(data[0], data[1]) + 2); 129 | break; 130 | default: 131 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 132 | return; 133 | } 134 | 135 | short offset = Util.getShort(apduBuffer, ISO7816.OFFSET_P1); 136 | 137 | if (offset < 0 || offset >= dataLen) { 138 | ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); 139 | } 140 | 141 | short le = apdu.setOutgoing(); 142 | if (le > NDEF_READ_SIZE) { 143 | le = NDEF_READ_SIZE; 144 | } 145 | 146 | if((short)(offset + le) >= dataLen) { 147 | le = (short)(dataLen - offset); 148 | } 149 | 150 | apdu.setOutgoingLength(le); 151 | apdu.sendBytesLong(data, offset, le); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/im/status/keycard/IdentApplet.java: -------------------------------------------------------------------------------- 1 | package im.status.keycard; 2 | 3 | import javacard.framework.*; 4 | import javacard.security.*; 5 | 6 | /** 7 | * The applet's main class. All incoming commands a processed by this class. 8 | */ 9 | public class IdentApplet extends Applet { 10 | static final byte TLV_CERT = (byte) 0x8A; 11 | static final byte CERT_VALID = (byte) 0xAA; 12 | 13 | static final byte INS_IDENTIFY_CARD = (byte) 0x14; 14 | 15 | /** 16 | * Invoked during applet installation. Creates an instance of this class. The installation parameters are passed in 17 | * the given buffer. 18 | * 19 | * @param bArray installation parameters buffer 20 | * @param bOffset offset where the installation parameters begin 21 | * @param bLength length of the installation parameters 22 | */ 23 | public static void install(byte[] bArray, short bOffset, byte bLength) { 24 | new IdentApplet(bArray, bOffset, bLength); 25 | } 26 | 27 | /** 28 | * Application constructor. All memory allocation is done here. The reason for this is two-fold: first the card might 29 | * not have Garbage Collection so dynamic allocation will eventually eat all memory. The second reason is to be sure 30 | * that if the application installs successfully, there is no risk of running out of memory because of other applets 31 | * allocating memory. The constructor also registers the applet with the JCRE so that it becomes selectable. 32 | * 33 | * @param bArray installation parameters buffer 34 | * @param bOffset offset where the installation parameters begin 35 | * @param bLength length of the installation parameters 36 | */ 37 | public IdentApplet(byte[] bArray, short bOffset, byte bLength) { 38 | SharedMemory.idPrivate = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, SECP256k1.SECP256K1_KEY_SIZE, false); 39 | SECP256k1.setCurveParameters(SharedMemory.idPrivate); 40 | SharedMemory.idCert[0] = 0; 41 | register(bArray, (short) (bOffset + 1), bArray[bOffset]); 42 | } 43 | 44 | /** 45 | * This method is called on every incoming APDU. This method is just a dispatcher which invokes the correct method 46 | * depending on the INS of the APDU. 47 | * 48 | * @param apdu the JCRE-owned APDU object. 49 | * @throws ISOException any processing error 50 | */ 51 | public void process(APDU apdu) throws ISOException { 52 | if (selectingApplet()) { 53 | processSelect(apdu); 54 | return; 55 | } 56 | 57 | byte[] apduBuffer = apdu.getBuffer(); 58 | 59 | switch (apduBuffer[ISO7816.OFFSET_INS]) { 60 | case KeycardApplet.INS_STORE_DATA: 61 | processStoreData(apdu); 62 | break; 63 | default: 64 | ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); 65 | break; 66 | } 67 | } 68 | 69 | private void processSelect(APDU apdu) { 70 | byte[] apduBuffer = apdu.getBuffer(); 71 | apdu.setIncomingAndReceive(); 72 | 73 | if (SharedMemory.idCert[0] == CERT_VALID) { 74 | Util.arrayCopyNonAtomic(SharedMemory.idCert, (short) 1, apduBuffer, (short) 0, SharedMemory.CERT_LEN); 75 | apdu.setOutgoingAndSend((short) 0, SharedMemory.CERT_LEN); 76 | } 77 | 78 | } 79 | 80 | private void processStoreData(APDU apdu) { 81 | if (SharedMemory.idCert[0] == CERT_VALID) { 82 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 83 | } 84 | 85 | byte[] apduBuffer = apdu.getBuffer(); 86 | apdu.setIncomingAndReceive(); 87 | 88 | if (Util.makeShort((byte) 0, apduBuffer[ISO7816.OFFSET_LC]) != (SharedMemory.CERT_LEN + Crypto.KEY_SECRET_SIZE)) { 89 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 90 | } 91 | 92 | Util.arrayCopyNonAtomic(apduBuffer, ISO7816.OFFSET_CDATA, SharedMemory.idCert, (short) 1, SharedMemory.CERT_LEN); 93 | SharedMemory.idPrivate.setS(apduBuffer, (short) (ISO7816.OFFSET_CDATA + SharedMemory.CERT_LEN), Crypto.KEY_SECRET_SIZE); 94 | SharedMemory.idCert[0] = CERT_VALID; 95 | } 96 | 97 | /** 98 | * Processes the IDENTIFY CARD command according to the application's specifications. 99 | * 100 | * @param apdu the JCRE-owned APDU object. 101 | */ 102 | static void identifyCard(APDU apdu, SecureChannel secureChannel, Signature signature) { 103 | byte[] apduBuffer = apdu.getBuffer(); 104 | 105 | short len; 106 | 107 | if (secureChannel != null && secureChannel.isOpen()) { 108 | len = secureChannel.preprocessAPDU(apduBuffer); 109 | } else { 110 | len = (short) (apduBuffer[ISO7816.OFFSET_LC] & (short) 0xff); 111 | } 112 | 113 | if (SharedMemory.idCert[0] != CERT_VALID) { 114 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 115 | } 116 | 117 | if (len != MessageDigest.LENGTH_SHA_256) { 118 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 119 | } 120 | 121 | short off = SecureChannel.SC_OUT_OFFSET; 122 | apduBuffer[off++] = KeycardApplet.TLV_SIGNATURE_TEMPLATE; 123 | apduBuffer[off++] = (byte) 0x81; 124 | off++; 125 | apduBuffer[off++] = TLV_CERT; 126 | apduBuffer[off++] = (byte) SharedMemory.CERT_LEN; 127 | Util.arrayCopyNonAtomic(SharedMemory.idCert, (short) 1, apduBuffer, off, SharedMemory.CERT_LEN); 128 | off += SharedMemory.CERT_LEN; 129 | 130 | short outLen = (short)(SharedMemory.CERT_LEN + 5); 131 | signature.init(SharedMemory.idPrivate, Signature.MODE_SIGN); 132 | outLen += signature.signPreComputedHash(apduBuffer, ISO7816.OFFSET_CDATA, MessageDigest.LENGTH_SHA_256, apduBuffer, off); 133 | 134 | apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 2)] = (byte)(outLen - 3); 135 | 136 | if (secureChannel != null && secureChannel.isOpen()) { 137 | secureChannel.respond(apdu, outLen, ISO7816.SW_NO_ERROR); 138 | } else { 139 | apdu.setOutgoingAndSend(SecureChannel.SC_OUT_OFFSET, outLen); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/main/java/im/status/keycard/SECP256k1.java: -------------------------------------------------------------------------------- 1 | package im.status.keycard; 2 | 3 | import javacard.security.ECKey; 4 | import javacard.security.ECPrivateKey; 5 | import javacard.security.KeyAgreement; 6 | import javacard.security.KeyBuilder; 7 | 8 | /** 9 | * Utility methods to work with the SECP256k1 curve. This class is not meant to be instantiated, but its init method 10 | * must be called during applet installation. 11 | */ 12 | public class SECP256k1 { 13 | static final byte SECP256K1_FP[] = { 14 | (byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF, 15 | (byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF, 16 | (byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF, 17 | (byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFE,(byte)0xFF,(byte)0xFF,(byte)0xFC,(byte)0x2F 18 | }; 19 | static final byte SECP256K1_A[] = { 20 | (byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00, 21 | (byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00, 22 | (byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00, 23 | (byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00 24 | }; 25 | static final byte SECP256K1_B[] = { 26 | (byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00, 27 | (byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00, 28 | (byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00, 29 | (byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x07 30 | }; 31 | static final byte SECP256K1_G[] = { 32 | (byte)0x04, 33 | (byte)0x79,(byte)0xBE,(byte)0x66,(byte)0x7E,(byte)0xF9,(byte)0xDC,(byte)0xBB,(byte)0xAC, 34 | (byte)0x55,(byte)0xA0,(byte)0x62,(byte)0x95,(byte)0xCE,(byte)0x87,(byte)0x0B,(byte)0x07, 35 | (byte)0x02,(byte)0x9B,(byte)0xFC,(byte)0xDB,(byte)0x2D,(byte)0xCE,(byte)0x28,(byte)0xD9, 36 | (byte)0x59,(byte)0xF2,(byte)0x81,(byte)0x5B,(byte)0x16,(byte)0xF8,(byte)0x17,(byte)0x98, 37 | (byte)0x48,(byte)0x3A,(byte)0xDA,(byte)0x77,(byte)0x26,(byte)0xA3,(byte)0xC4,(byte)0x65, 38 | (byte)0x5D,(byte)0xA4,(byte)0xFB,(byte)0xFC,(byte)0x0E,(byte)0x11,(byte)0x08,(byte)0xA8, 39 | (byte)0xFD,(byte)0x17,(byte)0xB4,(byte)0x48,(byte)0xA6,(byte)0x85,(byte)0x54,(byte)0x19, 40 | (byte)0x9C,(byte)0x47,(byte)0xD0,(byte)0x8F,(byte)0xFB,(byte)0x10,(byte)0xD4,(byte)0xB8 41 | }; 42 | static final byte SECP256K1_R[] = { 43 | (byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF, 44 | (byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFE, 45 | (byte)0xBA,(byte)0xAE,(byte)0xDC,(byte)0xE6,(byte)0xAF,(byte)0x48,(byte)0xA0,(byte)0x3B, 46 | (byte)0xBF,(byte)0xD2,(byte)0x5E,(byte)0x8C,(byte)0xD0,(byte)0x36,(byte)0x41,(byte)0x41 47 | }; 48 | 49 | static final byte SECP256K1_K = (byte)0x01; 50 | 51 | static final short SECP256K1_KEY_SIZE = 256; 52 | 53 | private static final byte ALG_EC_SVDP_DH_PLAIN_XY = 6; // constant from JavaCard 3.0.5 54 | 55 | 56 | private KeyAgreement ecPointMultiplier; 57 | ECPrivateKey tmpECPrivateKey; 58 | 59 | /** 60 | * Allocates objects needed by this class. Must be invoked during the applet installation exactly 1 time. 61 | */ 62 | SECP256k1() { 63 | this.ecPointMultiplier = KeyAgreement.getInstance(ALG_EC_SVDP_DH_PLAIN_XY, false); 64 | this.tmpECPrivateKey = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, SECP256K1_KEY_SIZE, false); 65 | setCurveParameters(tmpECPrivateKey); 66 | } 67 | 68 | /** 69 | * Sets the SECP256k1 curve parameters to the given ECKey (public or private). 70 | * 71 | * @param key the key where the curve parameters must be set 72 | */ 73 | static void setCurveParameters(ECKey key) { 74 | key.setA(SECP256K1_A, (short) 0x00, (short) SECP256K1_A.length); 75 | key.setB(SECP256K1_B, (short) 0x00, (short) SECP256K1_B.length); 76 | key.setFieldFP(SECP256K1_FP, (short) 0x00, (short) SECP256K1_FP.length); 77 | key.setG(SECP256K1_G, (short) 0x00, (short) SECP256K1_G.length); 78 | key.setR(SECP256K1_R, (short) 0x00, (short) SECP256K1_R.length); 79 | key.setK(SECP256K1_K); 80 | } 81 | 82 | /** 83 | * Derives the public key from the given private key and outputs it in the pubOut buffer. This is done by multiplying 84 | * the private key by the G point of the curve. 85 | * 86 | * @param privateKey the private key 87 | * @param pubOut the output buffer for the public key 88 | * @param pubOff the offset in pubOut 89 | * @return the length of the public key 90 | */ 91 | short derivePublicKey(ECPrivateKey privateKey, byte[] pubOut, short pubOff) { 92 | return multiplyPoint(privateKey, SECP256K1_G, (short) 0, (short) SECP256K1_G.length, pubOut, pubOff); 93 | } 94 | 95 | 96 | /** 97 | * Derives the public key from the given private key and outputs it in the pubOut buffer. This is done by multiplying 98 | * the private key by the G point of the curve. 99 | * 100 | * @param privateKey the private key 101 | * @param pubOut the output buffer for the public key 102 | * @param pubOff the offset in pubOut 103 | * @return the length of the public key 104 | */ 105 | short derivePublicKey(byte[] privateKey, short privOff, byte[] pubOut, short pubOff) { 106 | tmpECPrivateKey.setS(privateKey, privOff, (short)(SECP256K1_KEY_SIZE/8)); 107 | return derivePublicKey(tmpECPrivateKey, pubOut, pubOff); 108 | } 109 | 110 | /** 111 | * Multiplies a scalar in the form of a private key by the given point. Internally uses a special version of EC-DH 112 | * supported since JavaCard 3.0.5 which outputs both X and Y in their uncompressed form. 113 | * 114 | * @param privateKey the scalar in a private key object 115 | * @param point the point to multiply 116 | * @param pointOff the offset of the point 117 | * @param pointLen the length of the point 118 | * @param out the output buffer 119 | * @param outOff the offset in the output buffer 120 | * @return the length of the data written in the out buffer 121 | */ 122 | short multiplyPoint(ECPrivateKey privateKey, byte[] point, short pointOff, short pointLen, byte[] out, short outOff) { 123 | ecPointMultiplier.init(privateKey); 124 | return ecPointMultiplier.generateSecret(point, pointOff, pointLen, out, outOff); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/im/status/keycard/Crypto.java: -------------------------------------------------------------------------------- 1 | package im.status.keycard; 2 | 3 | import javacard.framework.JCSystem; 4 | import javacard.framework.Util; 5 | import javacard.security.*; 6 | import javacardx.crypto.Cipher; 7 | 8 | /** 9 | * Crypto utilities, mostly BIP32 related. The init method must be called during application installation. This class 10 | * is not meant to be instantiated. 11 | */ 12 | public class Crypto { 13 | final static public short AES_BLOCK_SIZE = 16; 14 | 15 | final static short KEY_SECRET_SIZE = 32; 16 | final static short KEY_PUB_SIZE = 65; 17 | final static short KEY_DERIVATION_SCRATCH_SIZE = 37; 18 | final static private short HMAC_OUT_SIZE = 64; 19 | 20 | final static private byte[] MAX_S = { (byte) 0x7F, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x5D, (byte) 0x57, (byte) 0x6E, (byte) 0x73, (byte) 0x57, (byte) 0xA4, (byte) 0x50, (byte) 0x1D, (byte) 0xDF, (byte) 0xE9, (byte) 0x2F, (byte) 0x46, (byte) 0x68, (byte) 0x1B, (byte) 0x20, (byte) 0xA0 }; 21 | final static private byte[] S_SUB = { (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFE, (byte) 0xBA, (byte) 0xAE, (byte) 0xDC, (byte) 0xE6, (byte) 0xAF, (byte) 0x48, (byte) 0xA0, (byte) 0x3B, (byte) 0xBF, (byte) 0xD2, (byte) 0x5E, (byte) 0x8C, (byte) 0xD0, (byte) 0x36, (byte) 0x41, (byte) 0x41 }; 22 | 23 | final static private byte HMAC_IPAD = (byte) 0x36; 24 | final static private byte HMAC_OPAD = (byte) 0x5c; 25 | final static private short HMAC_BLOCK_SIZE = (short) 128; 26 | 27 | final static private byte[] KEY_BITCOIN_SEED = {'B', 'i', 't', 'c', 'o', 'i', 'n', ' ', 's', 'e', 'e', 'd'}; 28 | 29 | // The below 5 objects can be accessed anywhere from the entire applet 30 | RandomData random; 31 | KeyAgreement ecdh; 32 | MessageDigest sha256; 33 | MessageDigest sha512; 34 | Cipher aesCbcIso9797m2; 35 | 36 | private Signature hmacSHA512; 37 | private HMACKey hmacKey; 38 | 39 | private byte[] hmacBlock; 40 | 41 | Crypto() { 42 | random = RandomData.getInstance(RandomData.ALG_SECURE_RANDOM); 43 | sha256 = MessageDigest.getInstance(MessageDigest.ALG_SHA_256, false); 44 | ecdh = KeyAgreement.getInstance(KeyAgreement.ALG_EC_SVDP_DH_PLAIN, false); 45 | sha512 = MessageDigest.getInstance(MessageDigest.ALG_SHA_512, false); 46 | aesCbcIso9797m2 = Cipher.getInstance(Cipher.ALG_AES_CBC_ISO9797_M2,false); 47 | 48 | try { 49 | hmacSHA512 = Signature.getInstance(Signature.ALG_HMAC_SHA_512, false); 50 | hmacKey = (HMACKey) KeyBuilder.buildKey(KeyBuilder.TYPE_HMAC_TRANSIENT_DESELECT, KeyBuilder.LENGTH_AES_256, false); 51 | } catch (CryptoException e) { 52 | hmacSHA512 = null; 53 | hmacBlock = JCSystem.makeTransientByteArray(HMAC_BLOCK_SIZE, JCSystem.CLEAR_ON_RESET); 54 | } 55 | 56 | } 57 | 58 | boolean bip32IsHardened(byte[] i, short iOff) { 59 | return (i[iOff] & (byte) 0x80) == (byte) 0x80; 60 | } 61 | 62 | /** 63 | * Derives a private key according to the algorithm defined in BIP32. The BIP32 specifications define some checks 64 | * to be performed on the derived keys. In the very unlikely event that these checks fail this key is not considered 65 | * to be valid so the derived key is discarded and this method returns false. 66 | * 67 | * @param i the buffer containing the key path element (a 32-bit big endian integer) 68 | * @param iOff the offset in the buffer 69 | * @return true if successful, false otherwise 70 | */ 71 | boolean bip32CKDPriv(byte[] i, short iOff, byte[] scratch, short scratchOff, byte[] data, short dataOff, byte[] output, short outOff) { 72 | short off = scratchOff; 73 | 74 | if (bip32IsHardened(i, iOff)) { 75 | scratch[off++] = 0; 76 | off = Util.arrayCopyNonAtomic(data, dataOff, scratch, off, KEY_SECRET_SIZE); 77 | } else { 78 | scratch[off++] = ((data[(short) (dataOff + KEY_SECRET_SIZE + KEY_SECRET_SIZE + KEY_PUB_SIZE - 1)] & 1) != 0 ? (byte) 0x03 : (byte) 0x02); 79 | off = Util.arrayCopyNonAtomic(data, (short) (dataOff + KEY_SECRET_SIZE + KEY_SECRET_SIZE + 1), scratch, off, KEY_SECRET_SIZE); 80 | } 81 | 82 | off = Util.arrayCopyNonAtomic(i, iOff, scratch, off, (short) 4); 83 | 84 | hmacSHA512(data, (short)(dataOff + KEY_SECRET_SIZE), KEY_SECRET_SIZE, scratch, scratchOff, (short)(off - scratchOff), output, outOff); 85 | 86 | if (ucmp256(output, outOff, SECP256k1.SECP256K1_R, (short) 0) >= 0) { 87 | return false; 88 | } 89 | 90 | addm256(output, outOff, data, dataOff, SECP256k1.SECP256K1_R, (short) 0, output, outOff); 91 | 92 | return !isZero256(output, outOff); 93 | } 94 | 95 | /** 96 | * Applies the algorithm for master key derivation defined by BIP32 to the binary seed provided as input. 97 | * 98 | * @param seed the binary seed 99 | * @param seedOff the offset of the binary seed 100 | * @param seedSize the size of the binary seed 101 | * @param masterKey the output buffer 102 | * @param keyOff the offset in the output buffer 103 | */ 104 | void bip32MasterFromSeed(byte[] seed, short seedOff, short seedSize, byte[] masterKey, short keyOff) { 105 | hmacSHA512(KEY_BITCOIN_SEED, (short) 0, (short) KEY_BITCOIN_SEED.length, seed, seedOff, seedSize, masterKey, keyOff); 106 | } 107 | 108 | /** 109 | * Fixes the S value of the signature as described in BIP-62 to avoid malleable signatures. It also fixes the all 110 | * internal TLV length fields. Returns the number of bytes by which the overall signature length changed (0 or -1). 111 | * 112 | * @param sig the signature 113 | * @param off the offset 114 | * @return the number of bytes by which the signature length changed 115 | */ 116 | short fixS(byte[] sig, short off) { 117 | short sOff = (short) (sig[(short) (off + 3)] + (short) (off + 5)); 118 | short ret = 0; 119 | 120 | if (sig[sOff] == 33) { 121 | Util.arrayCopyNonAtomic(sig, (short) (sOff + 2), sig, (short) (sOff + 1), (short) 32); 122 | sig[sOff] = 32; 123 | sig[(short)(off + 1)]--; 124 | ret = -1; 125 | } 126 | 127 | sOff++; 128 | 129 | if (ret == -1 || ucmp256(sig, sOff, MAX_S, (short) 0) > 0) { 130 | sub256(S_SUB, (short) 0, sig, sOff, sig, sOff); 131 | } 132 | 133 | return ret; 134 | } 135 | 136 | /** 137 | * Calculates the HMAC-SHA512 with the given key and data. Uses a software implementation which only requires SHA-512 138 | * to be supported on cards which do not have native HMAC-SHA512. 139 | * 140 | * @param key the HMAC key 141 | * @param keyOff the offset of the key 142 | * @param keyLen the length of the key 143 | * @param in the input data 144 | * @param inOff the offset of the input data 145 | * @param inLen the length of the input data 146 | * @param out the output buffer 147 | * @param outOff the offset in the output buffer 148 | */ 149 | private void hmacSHA512(byte[] key, short keyOff, short keyLen, byte[] in, short inOff, short inLen, byte[] out, short outOff) { 150 | if (hmacSHA512 != null) { 151 | hmacKey.setKey(key, keyOff, keyLen); 152 | hmacSHA512.init(hmacKey, Signature.MODE_SIGN); 153 | hmacSHA512.sign(in, inOff, inLen, out, outOff); 154 | } else { 155 | for (byte i = 0; i < 2; i++) { 156 | Util.arrayFillNonAtomic(hmacBlock, (short) 0, HMAC_BLOCK_SIZE, (i == 0 ? HMAC_IPAD : HMAC_OPAD)); 157 | 158 | for (short j = 0; j < keyLen; j++) { 159 | hmacBlock[j] ^= key[(short)(keyOff + j)]; 160 | } 161 | 162 | sha512.update(hmacBlock, (short) 0, HMAC_BLOCK_SIZE); 163 | 164 | if (i == 0) { 165 | sha512.doFinal(in, inOff, inLen, out, outOff); 166 | } else { 167 | sha512.doFinal(out, outOff, HMAC_OUT_SIZE, out, outOff); 168 | } 169 | } 170 | } 171 | } 172 | 173 | /** 174 | * Modulo addition of two 256-bit numbers. 175 | * 176 | * @param a the a operand 177 | * @param aOff the offset of the a operand 178 | * @param b the b operand 179 | * @param bOff the offset of the b operand 180 | * @param n the modulo 181 | * @param nOff the offset of the modulo 182 | * @param out the output buffer 183 | * @param outOff the offset in the output buffer 184 | */ 185 | private void addm256(byte[] a, short aOff, byte[] b, short bOff, byte[] n, short nOff, byte[] out, short outOff) { 186 | if ((add256(a, aOff, b, bOff, out, outOff) != 0) || (ucmp256(out, outOff, n, nOff) > 0)) { 187 | sub256(out, outOff, n, nOff, out, outOff); 188 | } 189 | } 190 | 191 | /** 192 | * Compares two 256-bit numbers. Returns a positive number if a > b, a negative one if a < b and 0 if a = b. 193 | * 194 | * @param a the a operand 195 | * @param aOff the offset of the a operand 196 | * @param b the b operand 197 | * @param bOff the offset of the b operand 198 | * @return the comparison result 199 | */ 200 | private short ucmp256(byte[] a, short aOff, byte[] b, short bOff) { 201 | short gt = 0; 202 | short eq = 1; 203 | 204 | for (short i = 0 ; i < 32; i++) { 205 | short l = (short)(a[(short)(aOff + i)] & 0x00ff); 206 | short r = (short)(b[(short)(bOff + i)] & 0x00ff); 207 | short d = (short)(r - l); 208 | short l_xor_r = (short)(l ^ r); 209 | short l_xor_d = (short)(l ^ d); 210 | short d_xored = (short)(d ^ (short)(l_xor_r & l_xor_d)); 211 | 212 | gt |= (d_xored >>> 15) & eq; 213 | eq &= ((short)(l_xor_r - 1) >>> 15); 214 | } 215 | 216 | return (short) ((gt + gt + eq) - 1); 217 | } 218 | 219 | /** 220 | * Checks if the given 256-bit number is 0. 221 | * 222 | * @param a the a operand 223 | * @param aOff the offset of the a operand 224 | * @return true if a is 0, false otherwise 225 | */ 226 | private boolean isZero256(byte[] a, short aOff) { 227 | byte acc = 0; 228 | 229 | for (short i = 0; i < 32; i++) { 230 | acc |= a[(short)(aOff + i)]; 231 | } 232 | 233 | return acc == 0; 234 | } 235 | 236 | /** 237 | * Addition of two 256-bit numbers. 238 | * 239 | * @param a the a operand 240 | * @param aOff the offset of the a operand 241 | * @param b the b operand 242 | * @param bOff the offset of the b operand 243 | * @param out the output buffer 244 | * @param outOff the offset in the output buffer 245 | * @return the carry of the addition 246 | */ 247 | private short add256(byte[] a, short aOff, byte[] b, short bOff, byte[] out, short outOff) { 248 | short outI = 0; 249 | for (short i = 31 ; i >= 0 ; i--) { 250 | outI = (short) ((short)(a[(short)(aOff + i)] & 0xFF) + (short)(b[(short)(bOff + i)] & 0xFF) + outI); 251 | out[(short)(outOff + i)] = (byte)outI ; 252 | outI = (short)(outI >> 8); 253 | } 254 | return outI; 255 | } 256 | 257 | /** 258 | * Subtraction of two 256-bit numbers. 259 | * 260 | * @param a the a operand 261 | * @param aOff the offset of the a operand 262 | * @param b the b operand 263 | * @param bOff the offset of the b operand 264 | * @param out the output buffer 265 | * @param outOff the offset in the output buffer 266 | * @return the carry of the subtraction 267 | */ 268 | private short sub256(byte[] a, short aOff, byte[] b, short bOff, byte[] out, short outOff) { 269 | short outI = 0; 270 | 271 | for (short i = 31 ; i >= 0 ; i--) { 272 | outI = (short) ((short)(a[(short)(aOff + i)] & 0xFF) - (short)(b[(short)(bOff + i)] & 0xFF) - outI); 273 | out[(short)(outOff + i)] = (byte)outI ; 274 | outI = (short)(((outI >> 8) != 0) ? 1 : 0); 275 | } 276 | 277 | return outI; 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/java/im/status/keycard/SecureChannel.java: -------------------------------------------------------------------------------- 1 | package im.status.keycard; 2 | 3 | import javacard.framework.*; 4 | import javacard.security.*; 5 | import javacardx.crypto.Cipher; 6 | 7 | /** 8 | * Implements all methods related to the secure channel as specified in the SECURE_CHANNEL.md document. 9 | */ 10 | public class SecureChannel { 11 | public static final short SC_KEY_LENGTH = 256; 12 | public static final short SC_SECRET_LENGTH = 32; 13 | public static final short PAIRING_KEY_LENGTH = SC_SECRET_LENGTH + 1; 14 | public static final short SC_BLOCK_SIZE = Crypto.AES_BLOCK_SIZE; 15 | public static final short SC_OUT_OFFSET = ISO7816.OFFSET_CDATA + (SC_BLOCK_SIZE * 2); 16 | 17 | public static final byte INS_OPEN_SECURE_CHANNEL = 0x10; 18 | public static final byte INS_MUTUALLY_AUTHENTICATE = 0x11; 19 | public static final byte INS_PAIR = 0x12; 20 | public static final byte INS_UNPAIR = 0x13; 21 | 22 | public static final byte PAIR_P1_FIRST_STEP = 0x00; 23 | public static final byte PAIR_P1_LAST_STEP = 0x01; 24 | 25 | public static final byte PAIR_P2_ANY = 0x00; 26 | public static final byte PAIR_P2_EPHEMERAL = 0x01; 27 | public static final byte PAIR_P2_PERSISTENT = 0x02; 28 | 29 | public static final byte PAIR_SLOT_EPHEMERAL = (byte) 0xff; 30 | public static final short PAIR_OFF_EPHEMERAL = (short) (0xff * PAIRING_KEY_LENGTH); 31 | 32 | // This is the maximum length acceptable for plaintext commands/responses for APDUs in short format 33 | public static final short SC_MAX_PLAIN_LENGTH = (short) 223; 34 | 35 | private AESKey scEncKey; 36 | private AESKey scMacKey; 37 | private Signature scMac; 38 | private KeyPair scKeypair; 39 | private byte[] secret; 40 | private byte[] pairingSecret; 41 | 42 | /* 43 | * To avoid overhead, the pairing keys are stored in a plain byte array as sequences of 33-bytes elements. The first 44 | * byte is 0 if the slot is free and 1 if used. The following 32 bytes are the actual key data. 45 | */ 46 | private byte[] pairingKeys; 47 | private byte[] ephemeralPairingKey; 48 | 49 | private short preassignedPairingOffset = -1; 50 | private byte remainingSlots; 51 | private boolean mutuallyAuthenticated = false; 52 | 53 | private Crypto crypto; 54 | 55 | /** 56 | * Instantiates a Secure Channel. All memory allocations (except pairing secret) needed for the secure channel are 57 | * performed here. The keypair used for the EC-DH algorithm is also generated here. 58 | */ 59 | public SecureChannel(byte pairingLimit, Crypto crypto, SECP256k1 secp256k1) { 60 | this.crypto = crypto; 61 | 62 | scMac = Signature.getInstance(Signature.ALG_AES_MAC_128_NOPAD, false); 63 | 64 | scEncKey = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES_TRANSIENT_DESELECT, KeyBuilder.LENGTH_AES_256, false); 65 | scMacKey = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES_TRANSIENT_DESELECT, KeyBuilder.LENGTH_AES_256, false); 66 | 67 | secret = JCSystem.makeTransientByteArray((short)(SC_SECRET_LENGTH * 2), JCSystem.CLEAR_ON_DESELECT); 68 | ephemeralPairingKey = JCSystem.makeTransientByteArray(PAIRING_KEY_LENGTH, JCSystem.CLEAR_ON_DESELECT); 69 | pairingKeys = new byte[(short)(PAIRING_KEY_LENGTH * pairingLimit)]; 70 | 71 | scKeypair = new KeyPair(KeyPair.ALG_EC_FP, SC_KEY_LENGTH); 72 | SECP256k1.setCurveParameters((ECKey) scKeypair.getPrivate()); 73 | SECP256k1.setCurveParameters((ECKey) scKeypair.getPublic()); 74 | scKeypair.genKeyPair(); 75 | 76 | remainingSlots = pairingLimit; 77 | pairingSecret = new byte[SC_SECRET_LENGTH]; 78 | } 79 | 80 | /** 81 | * Initializes the SecureChannel instance with the pairing secret. 82 | * 83 | * @param aPairingSecret the pairing secret 84 | * @param off the offset in the buffer 85 | */ 86 | public void initSecureChannel(byte[] aPairingSecret, short off) { 87 | Util.arrayCopy(aPairingSecret, off, pairingSecret, (short) 0, SC_SECRET_LENGTH); 88 | scKeypair.genKeyPair(); 89 | } 90 | 91 | /** 92 | * Decrypts the content of the APDU by generating an AES key using EC-DH. Usable only with specific commands. 93 | * @param apduBuffer the APDU buffer 94 | */ 95 | public void oneShotDecrypt(byte[] apduBuffer) { 96 | crypto.ecdh.init(scKeypair.getPrivate()); 97 | 98 | short off = (short)(ISO7816.OFFSET_CDATA + 1); 99 | try { 100 | crypto.ecdh.generateSecret(apduBuffer, off, apduBuffer[ISO7816.OFFSET_CDATA], secret, (short) 0); 101 | off = (short)(off + apduBuffer[ISO7816.OFFSET_CDATA]); 102 | } catch(Exception e) { 103 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 104 | return; 105 | } 106 | 107 | scEncKey.setKey(secret, (short) 0); 108 | crypto.aesCbcIso9797m2.init(scEncKey, Cipher.MODE_DECRYPT, apduBuffer, off, SC_BLOCK_SIZE); 109 | off = (short)(off + SC_BLOCK_SIZE); 110 | 111 | 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); 112 | } 113 | 114 | /** 115 | * Processes the OPEN SECURE CHANNEL command. 116 | * 117 | * @param apdu the JCRE-owned APDU object. 118 | */ 119 | public void openSecureChannel(APDU apdu) { 120 | preassignedPairingOffset = -1; 121 | mutuallyAuthenticated = false; 122 | 123 | byte[] apduBuffer = apdu.getBuffer(); 124 | byte[] pairKey; 125 | short pairingKeyOff; 126 | 127 | if (apduBuffer[ISO7816.OFFSET_P1] == PAIR_SLOT_EPHEMERAL) { 128 | pairKey = ephemeralPairingKey; 129 | pairingKeyOff = 0; 130 | } else { 131 | pairKey = pairingKeys; 132 | pairingKeyOff = checkPairingIndexAndGetOffset(apduBuffer[ISO7816.OFFSET_P1]); 133 | } 134 | 135 | if (pairKey[pairingKeyOff] != 1) { 136 | ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); 137 | } else { 138 | pairingKeyOff++; 139 | } 140 | 141 | crypto.ecdh.init(scKeypair.getPrivate()); 142 | short len; 143 | 144 | try { 145 | len = crypto.ecdh.generateSecret(apduBuffer, ISO7816.OFFSET_CDATA, apduBuffer[ISO7816.OFFSET_LC], secret, (short) 0); 146 | } catch(Exception e) { 147 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 148 | return; 149 | } 150 | 151 | crypto.random.generateData(apduBuffer, (short) 0, (short) (SC_SECRET_LENGTH + SC_BLOCK_SIZE)); 152 | crypto.sha512.update(secret, (short) 0, len); 153 | crypto.sha512.update(pairKey, pairingKeyOff, SC_SECRET_LENGTH); 154 | crypto.sha512.doFinal(apduBuffer, (short) 0, SC_SECRET_LENGTH, secret, (short) 0); 155 | scEncKey.setKey(secret, (short) 0); 156 | scMacKey.setKey(secret, SC_SECRET_LENGTH); 157 | Util.arrayCopyNonAtomic(apduBuffer, SC_SECRET_LENGTH, secret, (short) 0, SC_BLOCK_SIZE); 158 | Util.arrayFillNonAtomic(secret, SC_BLOCK_SIZE, (short) (secret.length - SC_BLOCK_SIZE), (byte) 0); 159 | apdu.setOutgoingAndSend((short) 0, (short) (SC_SECRET_LENGTH + SC_BLOCK_SIZE)); 160 | } 161 | 162 | /** 163 | * Processes the MUTUALLY AUTHENTICATE command. 164 | * 165 | * @param apdu the JCRE-owned APDU object. 166 | */ 167 | public void mutuallyAuthenticate(APDU apdu) { 168 | if (!scEncKey.isInitialized()) { 169 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 170 | } 171 | 172 | boolean oldMutuallyAuthenticated = mutuallyAuthenticated; 173 | mutuallyAuthenticated = true; 174 | 175 | byte[] apduBuffer = apdu.getBuffer(); 176 | short len = preprocessAPDU(apduBuffer); 177 | 178 | if (oldMutuallyAuthenticated) { 179 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 180 | } 181 | 182 | if (len != SC_SECRET_LENGTH) { 183 | reset(); 184 | ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); 185 | } 186 | 187 | crypto.random.generateData(apduBuffer, SC_OUT_OFFSET, SC_SECRET_LENGTH); 188 | respond(apdu, len, ISO7816.SW_NO_ERROR); 189 | } 190 | 191 | /** 192 | * Processes the PAIR command. 193 | * 194 | * @param apdu the JCRE-owned APDU object. 195 | */ 196 | public void pair(APDU apdu) { 197 | if (isOpen()) { 198 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 199 | } 200 | 201 | byte[] apduBuffer = apdu.getBuffer(); 202 | 203 | if (apduBuffer[ISO7816.OFFSET_LC] != SC_SECRET_LENGTH) { 204 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 205 | } 206 | 207 | short len; 208 | 209 | if (apduBuffer[ISO7816.OFFSET_P1] == PAIR_P1_FIRST_STEP) { 210 | len = pairStep1(apduBuffer); 211 | } else if ((apduBuffer[ISO7816.OFFSET_P1] == PAIR_P1_LAST_STEP) && (preassignedPairingOffset != -1)) { 212 | len = pairStep2(apduBuffer); 213 | } else { 214 | ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); 215 | return; 216 | } 217 | 218 | apdu.setOutgoingAndSend((short) 0, len); 219 | } 220 | 221 | /** 222 | * Performs the first step of pairing. In this step the card solves the challenge sent by the client, thus authenticating 223 | * itself to the client. At the same time, it creates a challenge for the client. This can only fail if the card has 224 | * already paired with the maximum allowed amount of clients. 225 | * 226 | * @param apduBuffer the APDU buffer 227 | * @return the length of the reply 228 | */ 229 | private short pairStep1(byte[] apduBuffer) { 230 | if (apduBuffer[ISO7816.OFFSET_P2] == PAIR_P2_EPHEMERAL) { 231 | preassignedPairingOffset = PAIR_OFF_EPHEMERAL; 232 | } else { 233 | preassignedPairingOffset = -1; 234 | 235 | for (short i = 0; i < (short) pairingKeys.length; i += PAIRING_KEY_LENGTH) { 236 | if (pairingKeys[i] == 0) { 237 | preassignedPairingOffset = i; 238 | break; 239 | } 240 | } 241 | } 242 | 243 | if (preassignedPairingOffset == -1) { 244 | if (apduBuffer[ISO7816.OFFSET_P2] == PAIR_P2_PERSISTENT) { 245 | ISOException.throwIt(ISO7816.SW_FILE_FULL); 246 | } else { 247 | preassignedPairingOffset = PAIR_OFF_EPHEMERAL; 248 | } 249 | } 250 | 251 | crypto.sha256.update(pairingSecret, (short) 0, SC_SECRET_LENGTH); 252 | crypto.sha256.doFinal(apduBuffer, ISO7816.OFFSET_CDATA, SC_SECRET_LENGTH, apduBuffer, (short) 0); 253 | crypto.random.generateData(secret, (short) 0, SC_SECRET_LENGTH); 254 | Util.arrayCopyNonAtomic(secret, (short) 0, apduBuffer, SC_SECRET_LENGTH, SC_SECRET_LENGTH); 255 | 256 | return (SC_SECRET_LENGTH * 2); 257 | } 258 | 259 | /** 260 | * Performs the last step of pairing. In this step the card verifies that the client has correctly solved its 261 | * challenge, authenticating it. It then proceeds to generate the pairing key and returns to the client the data 262 | * necessary to further establish a secure channel session. 263 | * 264 | * @param apduBuffer the APDU buffer 265 | * @return the length of the reply 266 | */ 267 | private short pairStep2(byte[] apduBuffer) { 268 | crypto.sha256.update(pairingSecret, (short) 0, SC_SECRET_LENGTH); 269 | crypto.sha256.doFinal(secret, (short) 0, SC_SECRET_LENGTH, secret, (short) 0); 270 | 271 | if (Util.arrayCompare(apduBuffer, ISO7816.OFFSET_CDATA, secret, (short) 0, SC_SECRET_LENGTH) != 0) { 272 | preassignedPairingOffset = -1; 273 | ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); 274 | } 275 | 276 | byte[] out; 277 | short outOff; 278 | 279 | if (preassignedPairingOffset == PAIR_OFF_EPHEMERAL) { 280 | out = ephemeralPairingKey; 281 | outOff = 0; 282 | } else { 283 | out = pairingKeys; 284 | outOff = preassignedPairingOffset; 285 | remainingSlots--; 286 | } 287 | 288 | crypto.random.generateData(apduBuffer, (short) 1, SC_SECRET_LENGTH); 289 | crypto.sha256.update(pairingSecret, (short) 0, SC_SECRET_LENGTH); 290 | crypto.sha256.doFinal(apduBuffer, (short) 1, SC_SECRET_LENGTH, out, (short) (outOff + 1)); 291 | out[outOff] = 1; 292 | apduBuffer[0] = (byte) (preassignedPairingOffset / PAIRING_KEY_LENGTH); 293 | 294 | preassignedPairingOffset = -1; 295 | 296 | return (SC_SECRET_LENGTH + 1); 297 | } 298 | 299 | /** 300 | * Processes the UNPAIR command. For security reasons the key is not only marked as free but also zero-ed out. This 301 | * method assumes that all security checks have been performed by the calling method. 302 | * 303 | * @param apduBuffer the APDU buffer 304 | */ 305 | public void unpair(byte[] apduBuffer) { 306 | if (apduBuffer[ISO7816.OFFSET_P1] == PAIR_SLOT_EPHEMERAL) { 307 | ephemeralPairingKey[0] = 0; 308 | return; 309 | } 310 | 311 | short off = checkPairingIndexAndGetOffset(apduBuffer[ISO7816.OFFSET_P1]); 312 | if (pairingKeys[off] == 1) { 313 | Util.arrayFillNonAtomic(pairingKeys, off, PAIRING_KEY_LENGTH, (byte) 0); 314 | remainingSlots++; 315 | } 316 | } 317 | 318 | /** 319 | * Decrypts the given APDU buffer. The plaintext is written in-place starting at the ISO7816.OFFSET_CDATA offset. The 320 | * MAC and padding are stripped. The LC byte is overwritten with the plaintext length. If the MAC cannot be verified 321 | * the secure channel is reset and the SW 0x6982 is thrown. 322 | * 323 | * @param apduBuffer the APDU buffer 324 | * @return the length of the decrypted 325 | */ 326 | public short preprocessAPDU(byte[] apduBuffer) { 327 | if (!isOpen()) { 328 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 329 | } 330 | 331 | short apduLen = (short)((short) apduBuffer[ISO7816.OFFSET_LC] & 0xff); 332 | 333 | if (!verifyAESMAC(apduBuffer, apduLen)) { 334 | reset(); 335 | ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); 336 | } 337 | 338 | crypto.aesCbcIso9797m2.init(scEncKey, Cipher.MODE_DECRYPT, secret, (short) 0, SC_BLOCK_SIZE); 339 | Util.arrayCopyNonAtomic(apduBuffer, ISO7816.OFFSET_CDATA, secret, (short) 0, SC_BLOCK_SIZE); 340 | short len = crypto.aesCbcIso9797m2.doFinal(apduBuffer, (short)(ISO7816.OFFSET_CDATA + SC_BLOCK_SIZE), (short) (apduLen - SC_BLOCK_SIZE), apduBuffer, ISO7816.OFFSET_CDATA); 341 | 342 | apduBuffer[ISO7816.OFFSET_LC] = (byte) len; 343 | 344 | return len; 345 | } 346 | 347 | /** 348 | * Verifies the AES CBC-MAC, either natively or with a software implementation. Can only be called from the 349 | * preprocessAPDU method since it expects the input buffer to be formatted in a particular way. 350 | * 351 | * @param apduBuffer the APDU buffer 352 | * @param apduLen the data len 353 | */ 354 | private boolean verifyAESMAC(byte[] apduBuffer, short apduLen) { 355 | scMac.init(scMacKey, Signature.MODE_VERIFY); 356 | scMac.update(apduBuffer, (short) 0, ISO7816.OFFSET_CDATA); 357 | scMac.update(secret, SC_BLOCK_SIZE, (short) (SC_BLOCK_SIZE - ISO7816.OFFSET_CDATA)); 358 | 359 | return scMac.verify(apduBuffer, (short) (ISO7816.OFFSET_CDATA + SC_BLOCK_SIZE), (short) (apduLen - SC_BLOCK_SIZE), apduBuffer, ISO7816.OFFSET_CDATA, SC_BLOCK_SIZE); 360 | } 361 | 362 | /** 363 | * Sends the response to the command. This the given SW is appended to the data automatically. The response data must 364 | * be placed starting at the SecureChannel.SC_OUT_OFFSET offset, to leave place for the SecureChannel-specific data at 365 | * the beginning of the APDU. 366 | * 367 | * @param apdu the APDU object 368 | * @param len the length of the plaintext 369 | */ 370 | public void respond(APDU apdu, short len, short sw) { 371 | byte[] apduBuffer = apdu.getBuffer(); 372 | 373 | Util.setShort(apduBuffer, (short) (SC_OUT_OFFSET + len), sw); 374 | len += 2; 375 | 376 | crypto.aesCbcIso9797m2.init(scEncKey, Cipher.MODE_ENCRYPT, secret, (short) 0, SC_BLOCK_SIZE); 377 | len = crypto.aesCbcIso9797m2.doFinal(apduBuffer, SC_OUT_OFFSET, len, apduBuffer, (short)(ISO7816.OFFSET_CDATA + SC_BLOCK_SIZE)); 378 | 379 | apduBuffer[0] = (byte) (len + SC_BLOCK_SIZE); 380 | 381 | computeAESMAC(len, apduBuffer); 382 | 383 | Util.arrayCopyNonAtomic(apduBuffer, ISO7816.OFFSET_CDATA, secret, (short) 0, SC_BLOCK_SIZE); 384 | 385 | len += SC_BLOCK_SIZE; 386 | apdu.setOutgoingAndSend(ISO7816.OFFSET_CDATA, len); 387 | } 388 | 389 | /** 390 | * Computes the AES CBC-MAC, either natively or with a software implementation. Can only be called from the respond 391 | * method since it expects the input buffer to be formatted in a particular way. 392 | * 393 | * @param len the data len 394 | * @param apduBuffer the APDU buffer 395 | */ 396 | private void computeAESMAC(short len, byte[] apduBuffer) { 397 | scMac.init(scMacKey, Signature.MODE_SIGN); 398 | scMac.update(apduBuffer, (short) 0, (short) 1); 399 | scMac.update(secret, SC_BLOCK_SIZE, (short) (SC_BLOCK_SIZE - 1)); 400 | scMac.sign(apduBuffer, (short) (ISO7816.OFFSET_CDATA + SC_BLOCK_SIZE), len, apduBuffer, ISO7816.OFFSET_CDATA); 401 | } 402 | 403 | /** 404 | * Copies the public key used for EC-DH in the given buffer. 405 | * 406 | * @param buf the buffer 407 | * @param off the offset in the buffer 408 | * @return the length of the public key 409 | */ 410 | public short copyPublicKey(byte[] buf, short off) { 411 | ECPublicKey pk = (ECPublicKey) scKeypair.getPublic(); 412 | return pk.getW(buf, off); 413 | } 414 | 415 | /** 416 | * Returns whether a secure channel is currently established or not. 417 | * @return whether a secure channel is currently established or not. 418 | */ 419 | public boolean isOpen() { 420 | return scEncKey.isInitialized() && scMacKey.isInitialized() && mutuallyAuthenticated; 421 | } 422 | 423 | /** 424 | * Returns the number of still available pairing slots. 425 | */ 426 | public byte getRemainingPairingSlots() { 427 | return remainingSlots; 428 | } 429 | 430 | /** 431 | * Resets the Secure Channel, invalidating the current session. If no session is opened, this does nothing. 432 | */ 433 | public void reset() { 434 | scEncKey.clearKey(); 435 | scMacKey.clearKey(); 436 | mutuallyAuthenticated = false; 437 | } 438 | 439 | /** 440 | * Updates the pairing secret. Does not affect existing pairings. 441 | * @param aPairingSecret the buffer 442 | * @param off the offset 443 | */ 444 | public void updatePairingSecret(byte[] aPairingSecret, byte off) { 445 | Util.arrayCopy(aPairingSecret, off, pairingSecret, (short) 0, SC_SECRET_LENGTH); 446 | } 447 | 448 | /** 449 | * Returns the offset in the pairingKey byte array of the pairing key with the given index. Throws 0x6A86 if the index 450 | * is invalid 451 | * 452 | * @param idx the index 453 | * @return the offset 454 | */ 455 | private short checkPairingIndexAndGetOffset(byte idx) { 456 | short off = (short) (idx * PAIRING_KEY_LENGTH); 457 | 458 | if (off >= ((short) pairingKeys.length)) { 459 | ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); 460 | } 461 | 462 | return off; 463 | } 464 | } 465 | -------------------------------------------------------------------------------- /src/main/java/im/status/keycard/KeycardApplet.java: -------------------------------------------------------------------------------- 1 | package im.status.keycard; 2 | 3 | import javacard.framework.*; 4 | import javacard.security.*; 5 | 6 | import static javacard.framework.ISO7816.OFFSET_P1; 7 | import static javacard.framework.ISO7816.OFFSET_P2; 8 | 9 | /** 10 | * The applet's main class. All incoming commands a processed by this class. 11 | */ 12 | public class KeycardApplet extends Applet { 13 | static final short APPLICATION_VERSION = (short) 0x0302; 14 | 15 | static final byte INS_GET_STATUS = (byte) 0xF2; 16 | static final byte INS_INIT = (byte) 0xFE; 17 | static final byte INS_FACTORY_RESET = (byte) 0xFD; 18 | static final byte INS_VERIFY_PIN = (byte) 0x20; 19 | static final byte INS_CHANGE_PIN = (byte) 0x21; 20 | static final byte INS_UNBLOCK_PIN = (byte) 0x22; 21 | static final byte INS_LOAD_KEY = (byte) 0xD0; 22 | static final byte INS_DERIVE_KEY = (byte) 0xD1; 23 | static final byte INS_GENERATE_MNEMONIC = (byte) 0xD2; 24 | static final byte INS_REMOVE_KEY = (byte) 0xD3; 25 | static final byte INS_GENERATE_KEY = (byte) 0xD4; 26 | static final byte INS_SIGN = (byte) 0xC0; 27 | static final byte INS_SET_PINLESS_PATH = (byte) 0xC1; 28 | static final byte INS_EXPORT_KEY = (byte) 0xC2; 29 | static final byte INS_GET_DATA = (byte) 0xCA; 30 | static final byte INS_STORE_DATA = (byte) 0xE2; 31 | 32 | static final short SW_REFERENCED_DATA_NOT_FOUND = (short) 0x6A88; 33 | 34 | static final byte PIN_MIN_RETRIES = 2; 35 | static final byte PIN_MAX_RETRIES = 10; 36 | static final byte PUK_MIN_RETRIES = 3; 37 | static final byte PUK_MAX_RETRIES = 12; 38 | 39 | static final byte PUK_LENGTH = 12; 40 | static final byte DEFAULT_PUK_MAX_RETRIES = 5; 41 | static final byte PIN_LENGTH = 6; 42 | static final byte DEFAULT_PIN_MAX_RETRIES = 3; 43 | static final byte KEY_PATH_MAX_DEPTH = 10; 44 | static final byte PAIRING_MAX_CLIENT_COUNT = 10; 45 | static final byte UID_LENGTH = 16; 46 | static final byte MAX_DATA_LENGTH = 127; 47 | 48 | static final short CHAIN_CODE_SIZE = 32; 49 | static final short KEY_UID_LENGTH = 32; 50 | static final short BIP39_SEED_SIZE = CHAIN_CODE_SIZE * 2; 51 | static final short BIP32_MIN_SEED_SIZE = 16; 52 | static final short BIP32_MAX_SEED_SIZE = BIP39_SEED_SIZE; 53 | 54 | static final byte GET_STATUS_P1_APPLICATION = 0x00; 55 | static final byte GET_STATUS_P1_KEY_PATH = 0x01; 56 | 57 | static final byte CHANGE_PIN_P1_USER_PIN = 0x00; 58 | static final byte CHANGE_PIN_P1_PUK = 0x01; 59 | static final byte CHANGE_PIN_P1_PAIRING_SECRET = 0x02; 60 | 61 | static final byte LOAD_KEY_P1_EC = 0x01; 62 | static final byte LOAD_KEY_P1_EXT_EC = 0x02; 63 | static final byte LOAD_KEY_P1_SEED = 0x03; 64 | 65 | static final byte DERIVE_P1_SOURCE_MASTER = (byte) 0x00; 66 | static final byte DERIVE_P1_SOURCE_PARENT = (byte) 0x40; 67 | static final byte DERIVE_P1_SOURCE_CURRENT = (byte) 0x80; 68 | static final byte DERIVE_P1_SOURCE_PINLESS = (byte) 0xC0; 69 | static final byte DERIVE_P1_SOURCE_MASK = (byte) 0xC0; 70 | 71 | static final byte GENERATE_MNEMONIC_P1_CS_MIN = 4; 72 | static final byte GENERATE_MNEMONIC_P1_CS_MAX = 8; 73 | static final byte GENERATE_MNEMONIC_TMP_OFF = SecureChannel.SC_OUT_OFFSET + ((((GENERATE_MNEMONIC_P1_CS_MAX * 32) + GENERATE_MNEMONIC_P1_CS_MAX) / 11) * 2); 74 | 75 | static final byte SIGN_P1_CURRENT_KEY = 0x00; 76 | static final byte SIGN_P1_DERIVE = 0x01; 77 | static final byte SIGN_P1_DERIVE_AND_MAKE_CURRENT = 0x02; 78 | static final byte SIGN_P1_PINLESS = 0x03; 79 | 80 | static final byte EXPORT_KEY_P1_CURRENT = 0x00; 81 | static final byte EXPORT_KEY_P1_DERIVE = 0x01; 82 | static final byte EXPORT_KEY_P1_DERIVE_AND_MAKE_CURRENT = 0x02; 83 | 84 | static final byte EXPORT_KEY_P2_PRIVATE_AND_PUBLIC = 0x00; 85 | static final byte EXPORT_KEY_P2_PUBLIC_ONLY = 0x01; 86 | static final byte EXPORT_KEY_P2_EXTENDED_PUBLIC = 0x02; 87 | 88 | static final byte STORE_DATA_P1_PUBLIC = 0x00; 89 | static final byte STORE_DATA_P1_NDEF = 0x01; 90 | static final byte STORE_DATA_P1_CASH = 0x02; 91 | 92 | static final byte FACTORY_RESET_P1_MAGIC = (byte) 0xAA; 93 | static final byte FACTORY_RESET_P2_MAGIC = 0x55; 94 | 95 | static final byte TLV_SIGNATURE_TEMPLATE = (byte) 0xA0; 96 | 97 | static final byte TLV_KEY_TEMPLATE = (byte) 0xA1; 98 | static final byte TLV_PUB_KEY = (byte) 0x80; 99 | static final byte TLV_PRIV_KEY = (byte) 0x81; 100 | static final byte TLV_CHAIN_CODE = (byte) 0x82; 101 | 102 | static final byte TLV_APPLICATION_STATUS_TEMPLATE = (byte) 0xA3; 103 | static final byte TLV_INT = (byte) 0x02; 104 | static final byte TLV_BOOL = (byte) 0x01; 105 | 106 | static final byte TLV_APPLICATION_INFO_TEMPLATE = (byte) 0xA4; 107 | static final byte TLV_UID = (byte) 0x8F; 108 | static final byte TLV_KEY_UID = (byte) 0x8E; 109 | static final byte TLV_CAPABILITIES = (byte) 0x8D; 110 | 111 | static final byte CAPABILITY_SECURE_CHANNEL = (byte) 0x01; 112 | static final byte CAPABILITY_KEY_MANAGEMENT = (byte) 0x02; 113 | static final byte CAPABILITY_CREDENTIALS_MANAGEMENT = (byte) 0x04; 114 | static final byte CAPABILITY_NDEF = (byte) 0x08; 115 | static final byte CAPABILITY_FACTORY_RESET = (byte) 0x10; 116 | 117 | static final byte APPLICATION_CAPABILITIES = (byte)(CAPABILITY_SECURE_CHANNEL | CAPABILITY_KEY_MANAGEMENT | CAPABILITY_CREDENTIALS_MANAGEMENT | CAPABILITY_NDEF | CAPABILITY_FACTORY_RESET); 118 | 119 | static final byte[] EIP_1581_PREFIX = { (byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2D}; 120 | 121 | private OwnerPIN pin; 122 | private OwnerPIN mainPIN; 123 | private OwnerPIN altPIN; 124 | private OwnerPIN puk; 125 | private byte[] uid; 126 | private SecureChannel secureChannel; 127 | 128 | private ECPublicKey masterPublic; 129 | private ECPrivateKey masterPrivate; 130 | private byte[] masterChainCode; 131 | private byte[] altChainCode; 132 | private byte[] chainCode; 133 | private boolean isExtended; 134 | 135 | private byte[] tmpPath; 136 | private short tmpPathLen; 137 | 138 | private byte[] keyPath; 139 | private short keyPathLen; 140 | 141 | private byte[] pinlessPath; 142 | private short pinlessPathLen; 143 | 144 | private Signature signature; 145 | 146 | private byte[] keyUID; 147 | 148 | private Crypto crypto; 149 | private SECP256k1 secp256k1; 150 | 151 | private byte[] derivationOutput; 152 | 153 | private byte[] data; 154 | 155 | /** 156 | * Invoked during applet installation. Creates an instance of this class. The installation parameters are passed in 157 | * the given buffer. 158 | * 159 | * @param bArray installation parameters buffer 160 | * @param bOffset offset where the installation parameters begin 161 | * @param bLength length of the installation parameters 162 | */ 163 | public static void install(byte[] bArray, short bOffset, byte bLength) { 164 | new KeycardApplet(bArray, bOffset, bLength); 165 | } 166 | 167 | /** 168 | * Application constructor. All memory allocation is done here and in the init function. The reason for this is 169 | * two-fold: first the card might not have Garbage Collection so dynamic allocation will eventually eat all memory. 170 | * The second reason is to be sure that if the application installs successfully, there is no risk of running out 171 | * of memory because of other applets allocating memory. The constructor also registers the applet with the JCRE so 172 | * that it becomes selectable. 173 | * 174 | * @param bArray installation parameters buffer 175 | * @param bOffset offset where the installation parameters begin 176 | * @param bLength length of the installation parameters 177 | */ 178 | public KeycardApplet(byte[] bArray, short bOffset, byte bLength) { 179 | crypto = new Crypto(); 180 | secp256k1 = new SECP256k1(); 181 | 182 | uid = new byte[UID_LENGTH]; 183 | crypto.random.generateData(uid, (short) 0, UID_LENGTH); 184 | 185 | masterPublic = (ECPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, SECP256k1.SECP256K1_KEY_SIZE, false); 186 | masterPrivate = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, SECP256k1.SECP256K1_KEY_SIZE, false); 187 | masterChainCode = new byte[CHAIN_CODE_SIZE]; 188 | altChainCode = new byte[CHAIN_CODE_SIZE]; 189 | chainCode = masterChainCode; 190 | 191 | keyPath = new byte[KEY_PATH_MAX_DEPTH * 4]; 192 | pinlessPath = new byte[KEY_PATH_MAX_DEPTH * 4]; 193 | tmpPath = JCSystem.makeTransientByteArray((short)(KEY_PATH_MAX_DEPTH * 4), JCSystem.CLEAR_ON_RESET); 194 | 195 | keyUID = new byte[KEY_UID_LENGTH]; 196 | 197 | resetCurveParameters(); 198 | 199 | signature = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false); 200 | 201 | derivationOutput = JCSystem.makeTransientByteArray((short) (Crypto.KEY_SECRET_SIZE + CHAIN_CODE_SIZE), JCSystem.CLEAR_ON_RESET); 202 | 203 | data = new byte[(short)(MAX_DATA_LENGTH + 1)]; 204 | 205 | register(bArray, (short) (bOffset + 1), bArray[bOffset]); 206 | } 207 | 208 | /** 209 | * This method is called on every incoming APDU. This method is just a dispatcher which invokes the correct method 210 | * depending on the INS of the APDU. 211 | * 212 | * @param apdu the JCRE-owned APDU object. 213 | * @throws ISOException any processing error 214 | */ 215 | public void process(APDU apdu) throws ISOException { 216 | // If we have no PIN it means we still have to initialize the applet. 217 | if (pin == null) { 218 | if (secureChannel == null) { 219 | secureChannel = new SecureChannel(PAIRING_MAX_CLIENT_COUNT, crypto, secp256k1); 220 | } 221 | processInit(apdu); 222 | return; 223 | } 224 | 225 | // Since selection can happen not only by a SELECT command, we check for that separately. 226 | if (selectingApplet()) { 227 | selectApplet(apdu); 228 | return; 229 | } 230 | 231 | apdu.setIncomingAndReceive(); 232 | byte[] apduBuffer = apdu.getBuffer(); 233 | 234 | try { 235 | switch (apduBuffer[ISO7816.OFFSET_INS]) { 236 | case SecureChannel.INS_OPEN_SECURE_CHANNEL: 237 | secureChannel.openSecureChannel(apdu); 238 | break; 239 | case SecureChannel.INS_MUTUALLY_AUTHENTICATE: 240 | secureChannel.mutuallyAuthenticate(apdu); 241 | break; 242 | case SecureChannel.INS_PAIR: 243 | secureChannel.pair(apdu); 244 | break; 245 | case SecureChannel.INS_UNPAIR: 246 | unpair(apdu); 247 | break; 248 | case IdentApplet.INS_IDENTIFY_CARD: 249 | IdentApplet.identifyCard(apdu, secureChannel, signature); 250 | break; 251 | case INS_GET_STATUS: 252 | getStatus(apdu); 253 | break; 254 | case INS_VERIFY_PIN: 255 | verifyPIN(apdu); 256 | break; 257 | case INS_CHANGE_PIN: 258 | changePIN(apdu); 259 | break; 260 | case INS_UNBLOCK_PIN: 261 | unblockPIN(apdu); 262 | break; 263 | case INS_LOAD_KEY: 264 | loadKey(apdu); 265 | break; 266 | case INS_DERIVE_KEY: 267 | deriveKey(apdu); 268 | break; 269 | case INS_GENERATE_MNEMONIC: 270 | generateMnemonic(apdu); 271 | break; 272 | case INS_REMOVE_KEY: 273 | removeKey(apdu); 274 | break; 275 | case INS_GENERATE_KEY: 276 | generateKey(apdu); 277 | break; 278 | case INS_SIGN: 279 | sign(apdu); 280 | break; 281 | case INS_SET_PINLESS_PATH: 282 | setPinlessPath(apdu); 283 | break; 284 | case INS_EXPORT_KEY: 285 | exportKey(apdu); 286 | break; 287 | case INS_GET_DATA: 288 | getData(apdu); 289 | break; 290 | case INS_STORE_DATA: 291 | storeData(apdu); 292 | break; 293 | case INS_FACTORY_RESET: 294 | factoryReset(apdu); 295 | return; 296 | default: 297 | ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); 298 | break; 299 | } 300 | } catch(ISOException sw) { 301 | handleException(apdu, sw.getReason()); 302 | } catch (CryptoException ce) { 303 | handleException(apdu, (short)(ISO7816.SW_UNKNOWN | ce.getReason())); 304 | } catch (Exception e) { 305 | handleException(apdu, ISO7816.SW_UNKNOWN); 306 | } 307 | 308 | if (shouldRespond(apdu)) { 309 | secureChannel.respond(apdu, (short) 0, ISO7816.SW_NO_ERROR); 310 | } 311 | } 312 | 313 | private void handleException(APDU apdu, short sw) { 314 | if (shouldRespond(apdu) && (sw != ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED)) { 315 | secureChannel.respond(apdu, (short) 0, sw); 316 | } else { 317 | ISOException.throwIt(sw); 318 | } 319 | } 320 | 321 | /** 322 | * Processes the init command, this is invoked only if the applet has not yet been personalized with secrets. 323 | * 324 | * @param apdu the JCRE-owned APDU object. 325 | */ 326 | private void processInit(APDU apdu) { 327 | byte[] apduBuffer = apdu.getBuffer(); 328 | apdu.setIncomingAndReceive(); 329 | 330 | if (selectingApplet()) { 331 | apduBuffer[0] = TLV_PUB_KEY; 332 | apduBuffer[1] = (byte) secureChannel.copyPublicKey(apduBuffer, (short) 2); 333 | apdu.setOutgoingAndSend((short) 0, (short)(apduBuffer[1] + 2)); 334 | } else if (apduBuffer[ISO7816.OFFSET_INS] == INS_INIT) { 335 | secureChannel.oneShotDecrypt(apduBuffer); 336 | 337 | byte defaultLimitsLen = (byte)(PIN_LENGTH + PUK_LENGTH + SecureChannel.SC_SECRET_LENGTH); 338 | byte withLimitsLen = (byte) (defaultLimitsLen + 2); 339 | byte withAltPIN = (byte) (withLimitsLen + 6); 340 | 341 | if (((apduBuffer[ISO7816.OFFSET_LC] != defaultLimitsLen) && (apduBuffer[ISO7816.OFFSET_LC] != withLimitsLen) && (apduBuffer[ISO7816.OFFSET_LC] != withAltPIN)) || !allDigits(apduBuffer, ISO7816.OFFSET_CDATA, (short)(PIN_LENGTH + PUK_LENGTH))) { 342 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 343 | } 344 | 345 | byte pinLimit; 346 | byte pukLimit; 347 | short altPinOff = (short)(ISO7816.OFFSET_CDATA + PIN_LENGTH); 348 | 349 | if (apduBuffer[ISO7816.OFFSET_LC] >= withLimitsLen) { 350 | pinLimit = apduBuffer[(short) (ISO7816.OFFSET_CDATA + defaultLimitsLen)]; 351 | pukLimit = apduBuffer[(short) (ISO7816.OFFSET_CDATA + defaultLimitsLen + 1)]; 352 | 353 | if (pinLimit < PIN_MIN_RETRIES || pinLimit > PIN_MAX_RETRIES || pukLimit < PUK_MIN_RETRIES || pukLimit > PUK_MAX_RETRIES) { 354 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 355 | } 356 | 357 | if (apduBuffer[ISO7816.OFFSET_LC] == withAltPIN) { 358 | altPinOff = (short)(ISO7816.OFFSET_CDATA + withLimitsLen); 359 | } 360 | } else { 361 | pinLimit = DEFAULT_PIN_MAX_RETRIES; 362 | pukLimit = DEFAULT_PUK_MAX_RETRIES; 363 | } 364 | 365 | secureChannel.initSecureChannel(apduBuffer, (short)(ISO7816.OFFSET_CDATA + PIN_LENGTH + PUK_LENGTH)); 366 | 367 | mainPIN = new OwnerPIN(pinLimit, PIN_LENGTH); 368 | mainPIN.update(apduBuffer, ISO7816.OFFSET_CDATA, PIN_LENGTH); 369 | 370 | altPIN = new OwnerPIN(pinLimit, PIN_LENGTH); 371 | altPIN.update(apduBuffer, altPinOff, PIN_LENGTH); 372 | 373 | puk = new OwnerPIN(pukLimit, PUK_LENGTH); 374 | puk.update(apduBuffer, (short)(ISO7816.OFFSET_CDATA + PIN_LENGTH), PUK_LENGTH); 375 | 376 | pin = mainPIN; 377 | } else if (apduBuffer[ISO7816.OFFSET_INS] == IdentApplet.INS_IDENTIFY_CARD) { 378 | IdentApplet.identifyCard(apdu, null, signature); 379 | } else { 380 | ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); 381 | } 382 | } 383 | 384 | private boolean shouldRespond(APDU apdu) { 385 | return secureChannel.isOpen() && (apdu.getCurrentState() != APDU.STATE_FULL_OUTGOING); 386 | } 387 | 388 | /** 389 | * Checks that the PIN is validated and if it is call the unpair method of the secure channel. If the PIN is not 390 | * validated the 0x6985 exception is thrown. 391 | * 392 | * @param apdu the JCRE-owned APDU object. 393 | */ 394 | private void unpair(APDU apdu) { 395 | byte[] apduBuffer = apdu.getBuffer(); 396 | secureChannel.preprocessAPDU(apduBuffer); 397 | 398 | if (pin.isValidated()) { 399 | secureChannel.unpair(apduBuffer); 400 | } else { 401 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 402 | } 403 | } 404 | 405 | /** 406 | * Invoked on applet (re-)selection. Aborts any in-progress signing session and sets PIN and PUK to not verified. 407 | * Responds with a SECP256k1 public key which the client must use to establish a secure channel. 408 | * 409 | * @param apdu the JCRE-owned APDU object. 410 | */ 411 | private void selectApplet(APDU apdu) { 412 | altPIN.reset(); 413 | mainPIN.reset(); 414 | puk.reset(); 415 | secureChannel.reset(); 416 | pin = mainPIN; 417 | 418 | byte[] apduBuffer = apdu.getBuffer(); 419 | 420 | short off = 0; 421 | 422 | apduBuffer[off++] = TLV_APPLICATION_INFO_TEMPLATE; 423 | 424 | if (masterPrivate.isInitialized()) { 425 | apduBuffer[off++] = (byte) 0x81; 426 | } 427 | 428 | short lenoff = off++; 429 | 430 | apduBuffer[off++] = TLV_UID; 431 | apduBuffer[off++] = UID_LENGTH; 432 | Util.arrayCopyNonAtomic(uid, (short) 0, apduBuffer, off, UID_LENGTH); 433 | off += UID_LENGTH; 434 | 435 | apduBuffer[off++] = TLV_PUB_KEY; 436 | short keyLength = secureChannel.copyPublicKey(apduBuffer, (short) (off + 1)); 437 | apduBuffer[off++] = (byte) keyLength; 438 | off += keyLength; 439 | 440 | apduBuffer[off++] = TLV_INT; 441 | apduBuffer[off++] = 2; 442 | Util.setShort(apduBuffer, off, APPLICATION_VERSION); 443 | off += 2; 444 | 445 | apduBuffer[off++] = TLV_INT; 446 | apduBuffer[off++] = 1; 447 | apduBuffer[off++] = secureChannel.getRemainingPairingSlots(); 448 | apduBuffer[off++] = TLV_KEY_UID; 449 | 450 | if (masterPrivate.isInitialized()) { 451 | apduBuffer[off++] = KEY_UID_LENGTH; 452 | Util.arrayCopyNonAtomic(keyUID, (short) 0, apduBuffer, off, KEY_UID_LENGTH); 453 | off += KEY_UID_LENGTH; 454 | } else { 455 | apduBuffer[off++] = 0; 456 | } 457 | 458 | apduBuffer[off++] = TLV_CAPABILITIES; 459 | apduBuffer[off++] = 1; 460 | apduBuffer[off++] = APPLICATION_CAPABILITIES; 461 | 462 | apduBuffer[lenoff] = (byte)(off - lenoff - 1); 463 | apdu.setOutgoingAndSend((short) 0, off); 464 | } 465 | 466 | /** 467 | * Processes the GET STATUS command according to the application's specifications. This command is always a Case-2 APDU. 468 | * Requires an open secure channel but does not check if the PIN has been verified. 469 | * 470 | * @param apdu the JCRE-owned APDU object. 471 | */ 472 | private void getStatus(APDU apdu) { 473 | byte[] apduBuffer = apdu.getBuffer(); 474 | secureChannel.preprocessAPDU(apduBuffer); 475 | 476 | short len; 477 | 478 | if (apduBuffer[OFFSET_P1] == GET_STATUS_P1_APPLICATION) { 479 | len = getApplicationStatus(apduBuffer, SecureChannel.SC_OUT_OFFSET); 480 | } else if (apduBuffer[OFFSET_P1] == GET_STATUS_P1_KEY_PATH) { 481 | len = getKeyStatus(apduBuffer, SecureChannel.SC_OUT_OFFSET); 482 | } else { 483 | ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); 484 | return; 485 | } 486 | 487 | secureChannel.respond(apdu, len, ISO7816.SW_NO_ERROR); 488 | } 489 | 490 | /** 491 | * Writes the Application Status Template to the APDU buffer. Invoked internally by the getStatus method. This 492 | * template is useful to understand if the card is blocked, if it has valid keys and if public key derivation is 493 | * supported. 494 | * 495 | * @param apduBuffer the APDU buffer 496 | * @param off the offset in the buffer where the application status template must be written at. 497 | * @return the length in bytes of the data to output 498 | */ 499 | private short getApplicationStatus(byte[] apduBuffer, short off) { 500 | apduBuffer[off++] = TLV_APPLICATION_STATUS_TEMPLATE; 501 | apduBuffer[off++] = 9; 502 | apduBuffer[off++] = TLV_INT; 503 | apduBuffer[off++] = 1; 504 | apduBuffer[off++] = pin.getTriesRemaining(); 505 | apduBuffer[off++] = TLV_INT; 506 | apduBuffer[off++] = 1; 507 | apduBuffer[off++] = puk.getTriesRemaining(); 508 | apduBuffer[off++] = TLV_BOOL; 509 | apduBuffer[off++] = 1; 510 | apduBuffer[off++] = masterPrivate.isInitialized() ? (byte) 0xFF : (byte) 0x00; 511 | 512 | return (short) (off - SecureChannel.SC_OUT_OFFSET); 513 | } 514 | 515 | /** 516 | * Writes the key path status to the APDU buffer. Invoked internally by the getStatus method. The key path indicates 517 | * at which point in the BIP32 hierarchy we are at. The data is unformatted and is simply a sequence of 32-bit 518 | * big endian integers. The Master key is not indicated so nothing will be written if no derivation has been performed. 519 | * However, because of the secure channel, the response will still contain the IV and the padding. 520 | * 521 | * @param apduBuffer the APDU buffer 522 | * @param off the offset in the buffer where the key status template must be written at. 523 | * @return the length in bytes of the data to output 524 | */ 525 | private short getKeyStatus(byte[] apduBuffer, short off) { 526 | Util.arrayCopyNonAtomic(keyPath, (short) 0, apduBuffer, off, keyPathLen); 527 | return keyPathLen; 528 | } 529 | 530 | /** 531 | * Processes the VERIFY PIN command. Requires a secure channel to be already open. If a PIN longer or shorter than 6 532 | * digits is provided, the method will still proceed with its verification and will decrease the remaining tries 533 | * counter. 534 | * 535 | * @param apdu the JCRE-owned APDU object. 536 | */ 537 | private void verifyPIN(APDU apdu) { 538 | byte[] apduBuffer = apdu.getBuffer(); 539 | byte len = (byte) secureChannel.preprocessAPDU(apduBuffer); 540 | 541 | if (!(len == PIN_LENGTH && allDigits(apduBuffer, ISO7816.OFFSET_CDATA, len))) { 542 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 543 | } 544 | 545 | short resp = mainPIN.check(apduBuffer, ISO7816.OFFSET_CDATA, len) ? (short) 1 : (short) 0; 546 | resp += altPIN.check(apduBuffer, ISO7816.OFFSET_CDATA, len) ? (short) 2 : (short) 0; 547 | 548 | switch(resp) { 549 | case 0: 550 | ISOException.throwIt((short)((short) 0x63c0 | (short) pin.getTriesRemaining())); 551 | break; 552 | case 1: 553 | chainCode = masterChainCode; 554 | altPIN.resetAndUnblock(); 555 | pin = mainPIN; 556 | break; 557 | case 2: 558 | case 3: // if pins are equal fake pin takes precedence 559 | chainCode = altChainCode; 560 | mainPIN.resetAndUnblock(); 561 | pin = altPIN; 562 | break; 563 | } 564 | } 565 | 566 | /** 567 | * Processes the CHANGE PIN command. Requires a secure channel to be already open and the user PIN to be verified. All 568 | * PINs have a fixed format which is verified by this method. 569 | * 570 | * @param apdu the JCRE-owned APDU object. 571 | */ 572 | private void changePIN(APDU apdu) { 573 | byte[] apduBuffer = apdu.getBuffer(); 574 | byte len = (byte) secureChannel.preprocessAPDU(apduBuffer); 575 | 576 | if (!pin.isValidated()) { 577 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 578 | } 579 | 580 | switch(apduBuffer[OFFSET_P1]) { 581 | case CHANGE_PIN_P1_USER_PIN: 582 | changeUserPIN(apduBuffer, len); 583 | break; 584 | case CHANGE_PIN_P1_PUK: 585 | changePUK(apduBuffer, len); 586 | break; 587 | case CHANGE_PIN_P1_PAIRING_SECRET: 588 | changePairingSecret(apduBuffer, len); 589 | break; 590 | default: 591 | ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); 592 | break; 593 | } 594 | } 595 | 596 | /** 597 | * Changes the user PIN. Called internally by CHANGE PIN 598 | * @param apduBuffer the APDU buffer 599 | * @param len the data length 600 | */ 601 | private void changeUserPIN(byte[] apduBuffer, byte len) { 602 | if (!(len == PIN_LENGTH && allDigits(apduBuffer, ISO7816.OFFSET_CDATA, len))) { 603 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 604 | } 605 | 606 | pin.update(apduBuffer, ISO7816.OFFSET_CDATA, len); 607 | pin.check(apduBuffer, ISO7816.OFFSET_CDATA, len); 608 | } 609 | 610 | /** 611 | * Changes the PUK. Called internally by CHANGE PIN 612 | * @param apduBuffer the APDU buffer 613 | * @param len the data length 614 | */ 615 | private void changePUK(byte[] apduBuffer, byte len) { 616 | if (!(len == PUK_LENGTH && allDigits(apduBuffer, ISO7816.OFFSET_CDATA, len))) { 617 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 618 | } 619 | 620 | puk.update(apduBuffer, ISO7816.OFFSET_CDATA, len); 621 | } 622 | 623 | /** 624 | * Changes the pairing secret. Called internally by CHANGE PIN 625 | * @param apduBuffer the APDU buffer 626 | * @param len the data length 627 | */ 628 | private void changePairingSecret(byte[] apduBuffer, byte len) { 629 | if (len != SecureChannel.SC_SECRET_LENGTH) { 630 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 631 | } 632 | 633 | secureChannel.updatePairingSecret(apduBuffer, ISO7816.OFFSET_CDATA); 634 | } 635 | 636 | /** 637 | * Processes the UNBLOCK PIN command. Requires a secure channel to be already open and the PIN to be blocked. The PUK 638 | * and the new PIN are sent in the same APDU with no separator. This is possible because the PUK is exactly 12 digits 639 | * long and the PIN is 6 digits long. If the data is not in the correct format (i.e: anything other than 18 digits), 640 | * PUK verification is not attempted, so the remaining tries counter of the PUK is not decreased. 641 | * 642 | * @param apdu the JCRE-owned APDU object. 643 | */ 644 | private void unblockPIN(APDU apdu) { 645 | byte[] apduBuffer = apdu.getBuffer(); 646 | byte len = (byte) secureChannel.preprocessAPDU(apduBuffer); 647 | 648 | if (pin.getTriesRemaining() != 0) { 649 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 650 | } 651 | 652 | if (!(len == (PUK_LENGTH + PIN_LENGTH) && allDigits(apduBuffer, ISO7816.OFFSET_CDATA, len))) { 653 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 654 | } 655 | 656 | if (!puk.check(apduBuffer, ISO7816.OFFSET_CDATA, PUK_LENGTH)) { 657 | ISOException.throwIt((short)((short) 0x63c0 | (short) puk.getTriesRemaining())); 658 | } 659 | 660 | altPIN.resetAndUnblock(); 661 | mainPIN.resetAndUnblock(); 662 | pin.update(apduBuffer, (short)(ISO7816.OFFSET_CDATA + PUK_LENGTH), PIN_LENGTH); 663 | pin.check(apduBuffer, (short)(ISO7816.OFFSET_CDATA + PUK_LENGTH), PIN_LENGTH); 664 | puk.reset(); 665 | } 666 | 667 | /** 668 | * Processes the LOAD KEY command. Requires a secure channel to be already open and the PIN to be verified. The key 669 | * being loaded will be treated as the master key. If the key is not in extended format (i.e: does not contain a chain 670 | * code) no further derivation will be possible. Loading a key resets the current key path and the loaded key becomes 671 | * the one used for signing. Transactions are used to make sure that either all key components are loaded correctly 672 | * or none is loaded at all. 673 | * 674 | * @param apdu the JCRE-owned APDU object. 675 | */ 676 | private void loadKey(APDU apdu) { 677 | byte[] apduBuffer = apdu.getBuffer(); 678 | secureChannel.preprocessAPDU(apduBuffer); 679 | 680 | if (!pin.isValidated()) { 681 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 682 | } 683 | 684 | switch (apduBuffer[OFFSET_P1]) { 685 | case LOAD_KEY_P1_EC: 686 | case LOAD_KEY_P1_EXT_EC: 687 | loadKeyPair(apduBuffer); 688 | break; 689 | case LOAD_KEY_P1_SEED: 690 | loadSeed(apduBuffer); 691 | break; 692 | default: 693 | ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); 694 | break; 695 | } 696 | 697 | secureChannel.respond(apdu, KEY_UID_LENGTH, ISO7816.SW_NO_ERROR); 698 | } 699 | 700 | /** 701 | * Generates the Key UID from the current master public key and responds to the command. 702 | * 703 | * @param apduBuffer the APDU buffer 704 | */ 705 | private void generateKeyUIDAndPrepareResponse(byte[] apduBuffer) { 706 | if (isExtended) { 707 | crypto.sha256.doFinal(masterChainCode, (short) 0, CHAIN_CODE_SIZE, altChainCode, (short) 0); 708 | } 709 | 710 | short pubLen = masterPublic.getW(apduBuffer, (short) 0); 711 | crypto.sha256.doFinal(apduBuffer, (short) 0, pubLen, keyUID, (short) 0); 712 | Util.arrayCopy(keyUID, (short) 0, apduBuffer, SecureChannel.SC_OUT_OFFSET, KEY_UID_LENGTH); 713 | } 714 | 715 | /** 716 | * Resets the status of the keys. This method must be called immediately before committing the transaction where key 717 | * manipulation has happened to be sure that the state is always consistent. 718 | */ 719 | private void resetKeyStatus() { 720 | keyPathLen = 0; 721 | pinlessPathLen = 0; 722 | } 723 | 724 | /** 725 | * Called internally by the loadKey method to load a key in the TLV format. The presence of the public key is optional. 726 | * The presence of the chain code determines whether the key is extended or not. 727 | * 728 | * @param apduBuffer the APDU buffer 729 | */ 730 | private void loadKeyPair(byte[] apduBuffer) { 731 | short pubOffset = (short)(ISO7816.OFFSET_CDATA + (apduBuffer[(short) (ISO7816.OFFSET_CDATA + 1)] == (byte) 0x81 ? 3 : 2)); 732 | short privOffset = (short)(pubOffset + apduBuffer[(short)(pubOffset + 1)] + 2); 733 | short chainOffset = (short)(privOffset + apduBuffer[(short)(privOffset + 1)] + 2); 734 | 735 | if (apduBuffer[pubOffset] != TLV_PUB_KEY) { 736 | chainOffset = privOffset; 737 | privOffset = pubOffset; 738 | pubOffset = -1; 739 | } 740 | 741 | if (!((apduBuffer[ISO7816.OFFSET_CDATA] == TLV_KEY_TEMPLATE) && (apduBuffer[privOffset] == TLV_PRIV_KEY))) { 742 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 743 | } 744 | 745 | JCSystem.beginTransaction(); 746 | 747 | try { 748 | isExtended = (apduBuffer[chainOffset] == TLV_CHAIN_CODE); 749 | 750 | masterPrivate.setS(apduBuffer, (short) (privOffset + 2), apduBuffer[(short) (privOffset + 1)]); 751 | 752 | if (isExtended) { 753 | if (apduBuffer[(short) (chainOffset + 1)] == CHAIN_CODE_SIZE) { 754 | Util.arrayCopy(apduBuffer, (short) (chainOffset + 2), masterChainCode, (short) 0, apduBuffer[(short) (chainOffset + 1)]); 755 | } else { 756 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 757 | } 758 | } 759 | 760 | short pubLen; 761 | 762 | if (pubOffset != -1) { 763 | pubLen = apduBuffer[(short) (pubOffset + 1)]; 764 | pubOffset = (short) (pubOffset + 2); 765 | } else { 766 | pubOffset = 0; 767 | pubLen = secp256k1.derivePublicKey(masterPrivate, apduBuffer, pubOffset); 768 | } 769 | 770 | masterPublic.setW(apduBuffer, pubOffset, pubLen); 771 | } catch (CryptoException e) { 772 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 773 | } 774 | 775 | resetKeyStatus(); 776 | generateKeyUIDAndPrepareResponse(apduBuffer); 777 | JCSystem.commitTransaction(); 778 | } 779 | 780 | /** 781 | * Called internally by the loadKey method to load a key from a sequence up to 64 bytes, possibly generated according 782 | * to the algorithms described in the BIP39 or SLIP39 specifications. 783 | * 784 | * @param apduBuffer the APDU buffer 785 | */ 786 | private void loadSeed(byte[] apduBuffer) { 787 | short seedLen = (short) apduBuffer[ISO7816.OFFSET_LC]; 788 | 789 | if ((seedLen < BIP32_MIN_SEED_SIZE) || (seedLen > BIP32_MAX_SEED_SIZE)) { 790 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 791 | } 792 | 793 | crypto.bip32MasterFromSeed(apduBuffer, (short) ISO7816.OFFSET_CDATA, seedLen, apduBuffer, (short) ISO7816.OFFSET_CDATA); 794 | 795 | JCSystem.beginTransaction(); 796 | isExtended = true; 797 | 798 | masterPrivate.setS(apduBuffer, (short) ISO7816.OFFSET_CDATA, CHAIN_CODE_SIZE); 799 | 800 | Util.arrayCopy(apduBuffer, (short) (ISO7816.OFFSET_CDATA + CHAIN_CODE_SIZE), masterChainCode, (short) 0, CHAIN_CODE_SIZE); 801 | short pubLen = secp256k1.derivePublicKey(masterPrivate, apduBuffer, (short) 0); 802 | 803 | masterPublic.setW(apduBuffer, (short) 0, pubLen); 804 | 805 | resetKeyStatus(); 806 | generateKeyUIDAndPrepareResponse(apduBuffer); 807 | JCSystem.commitTransaction(); 808 | } 809 | 810 | /** 811 | * Processes the DERIVE KEY command. Requires a secure channel to be already open and the PIN must be verified as well. 812 | * The master key must be already loaded and have a chain code. This function only updates the current path but does 813 | * not actually perform derivation, which is delayed to exporting/signing. 814 | * 815 | * @param apdu the JCRE-owned APDU object. 816 | */ 817 | private void deriveKey(APDU apdu) { 818 | byte[] apduBuffer = apdu.getBuffer(); 819 | short len = secureChannel.preprocessAPDU(apduBuffer); 820 | 821 | if (!pin.isValidated()) { 822 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 823 | } 824 | 825 | updateDerivationPath(apduBuffer, (short) 0, len, apduBuffer[OFFSET_P1]); 826 | commitTmpPath(); 827 | } 828 | 829 | /** 830 | * Updates the derivation path for a subsequent EXPORT KEY/SIGN APDU. Optionally stores the result in the current path. 831 | * 832 | * @param path the path 833 | * @param off the offset in the path 834 | * @param len the len of the path 835 | * @param source derivation source 836 | */ 837 | private void updateDerivationPath(byte[] path, short off, short len, byte source) { 838 | if (!isExtended) { 839 | if (len == 0) { 840 | tmpPathLen = 0; 841 | } else { 842 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 843 | } 844 | 845 | return; 846 | } 847 | 848 | short newPathLen; 849 | short pathLenOff; 850 | 851 | byte[] srcKeyPath = keyPath; 852 | 853 | switch (source) { 854 | case DERIVE_P1_SOURCE_MASTER: 855 | newPathLen = len; 856 | pathLenOff = 0; 857 | break; 858 | case DERIVE_P1_SOURCE_PARENT: 859 | if (keyPathLen < 4) { 860 | ISOException.throwIt(ISO7816.SW_WRONG_P1P2); 861 | } 862 | newPathLen = (short) (keyPathLen + len - 4); 863 | pathLenOff = (short) (keyPathLen - 4); 864 | break; 865 | case DERIVE_P1_SOURCE_CURRENT: 866 | newPathLen = (short) (keyPathLen + len); 867 | pathLenOff = keyPathLen; 868 | break; 869 | case DERIVE_P1_SOURCE_PINLESS: 870 | if (len != 0) { 871 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 872 | } 873 | srcKeyPath = pinlessPath; 874 | newPathLen = pinlessPathLen; 875 | pathLenOff = pinlessPathLen; 876 | break; 877 | default: 878 | ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); 879 | return; 880 | } 881 | 882 | if (((short) (len % 4) != 0) || (newPathLen > keyPath.length)) { 883 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 884 | } 885 | 886 | short pathOff = (short) (ISO7816.OFFSET_CDATA + off); 887 | 888 | Util.arrayCopyNonAtomic(srcKeyPath, (short) 0, tmpPath, (short) 0, pathLenOff); 889 | Util.arrayCopyNonAtomic(path, pathOff, tmpPath, pathLenOff, len); 890 | tmpPathLen = newPathLen; 891 | } 892 | 893 | /** 894 | * Makes the tmp path the current path. 895 | */ 896 | void commitTmpPath() { 897 | JCSystem.beginTransaction(); 898 | Util.arrayCopy(tmpPath, (short) 0, keyPath, (short) 0, tmpPathLen); 899 | keyPathLen = tmpPathLen; 900 | JCSystem.commitTransaction(); 901 | } 902 | 903 | /** 904 | * Internal derivation function, called by DERIVE KEY and EXPORT KEY 905 | * @param apduBuffer the APDU buffer 906 | * @param off the offset in the APDU buffer relative to the data field 907 | */ 908 | private void doDerive(byte[] apduBuffer, short off) { 909 | if (tmpPathLen == 0) { 910 | masterPrivate.getS(derivationOutput, (short) 0); 911 | return; 912 | } 913 | 914 | short scratchOff = (short) (ISO7816.OFFSET_CDATA + off); 915 | short dataOff = (short) (scratchOff + Crypto.KEY_DERIVATION_SCRATCH_SIZE); 916 | 917 | short pubKeyOff = (short) (dataOff + masterPrivate.getS(apduBuffer, dataOff)); 918 | pubKeyOff = Util.arrayCopyNonAtomic(chainCode, (short) 0, apduBuffer, pubKeyOff, CHAIN_CODE_SIZE); 919 | 920 | if (!crypto.bip32IsHardened(tmpPath, (short) 0)) { 921 | masterPublic.getW(apduBuffer, pubKeyOff); 922 | } else { 923 | apduBuffer[pubKeyOff] = 0; 924 | } 925 | 926 | for (short i = 0; i < tmpPathLen; i += 4) { 927 | if (i > 0) { 928 | Util.arrayCopyNonAtomic(derivationOutput, (short) 0, apduBuffer, dataOff, (short) (Crypto.KEY_SECRET_SIZE + CHAIN_CODE_SIZE)); 929 | 930 | if (!crypto.bip32IsHardened(tmpPath, i)) { 931 | secp256k1.derivePublicKey(apduBuffer, dataOff, apduBuffer, pubKeyOff); 932 | } else { 933 | apduBuffer[pubKeyOff] = 0; 934 | } 935 | } 936 | 937 | if (!crypto.bip32CKDPriv(tmpPath, i, apduBuffer, scratchOff, apduBuffer, dataOff, derivationOutput, (short) 0)) { 938 | ISOException.throwIt(ISO7816.SW_DATA_INVALID); 939 | } 940 | } 941 | } 942 | 943 | /** 944 | * Generates a mnemonic phrase according to the BIP39 specifications. Requires an open secure channel. Since embedding 945 | * the strings in the applet would be unreasonable, the data returned is actually a sequence of 16-bit big-endian 946 | * integers with values ranging from 0 to 2047. These numbers should be used by the client as indexes in their own 947 | * string tables which is used to actually generate the mnemonic phrase. 948 | * 949 | * The P1 parameter is the length of the checksum which indirectly also defines the length of the secret and finally 950 | * the number of generated words. Although using the length of the checksum as the defining parameter (as opposed to 951 | * the word count for example) might seem peculiar, this is done because it's valid values are strictly in the 952 | * inclusive range from 4 to 8 which makes it easy to validate input. 953 | * 954 | * @param apdu the JCRE-owned APDU object. 955 | */ 956 | private void generateMnemonic(APDU apdu) { 957 | byte[] apduBuffer = apdu.getBuffer(); 958 | secureChannel.preprocessAPDU(apduBuffer); 959 | 960 | short csLen = apduBuffer[OFFSET_P1]; 961 | 962 | if (csLen < GENERATE_MNEMONIC_P1_CS_MIN || csLen > GENERATE_MNEMONIC_P1_CS_MAX) { 963 | ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); 964 | } 965 | 966 | short entLen = (short) (csLen * 4); 967 | crypto.random.generateData(apduBuffer, GENERATE_MNEMONIC_TMP_OFF, entLen); 968 | crypto.sha256.doFinal(apduBuffer, GENERATE_MNEMONIC_TMP_OFF, entLen, apduBuffer, (short)(GENERATE_MNEMONIC_TMP_OFF + entLen)); 969 | entLen += GENERATE_MNEMONIC_TMP_OFF + 1; 970 | 971 | short outOff = SecureChannel.SC_OUT_OFFSET; 972 | short rShift = 0; 973 | short vp = 0; 974 | 975 | for (short i = GENERATE_MNEMONIC_TMP_OFF; i < entLen; i += 2) { 976 | short w = Util.getShort(apduBuffer, i); 977 | Util.setShort(apduBuffer, outOff, logicrShift((short) (vp | logicrShift(w, rShift)), (short) 5)); 978 | outOff += 2; 979 | rShift += 5; 980 | vp = (short) (w << (16 - rShift)); 981 | 982 | if (rShift >= 11) { 983 | Util.setShort(apduBuffer, outOff, logicrShift(vp, (short) 5)); 984 | outOff += 2; 985 | rShift = (short) (rShift - 11); 986 | vp = (short) (w << (16 - rShift)); 987 | } 988 | } 989 | 990 | if (csLen < 6) { 991 | outOff -= 2; // a last spurious 11 bit number will be generated when cs length is less than 6 because 16 - cs >= 11 992 | } 993 | 994 | secureChannel.respond(apdu, (short) (outOff - SecureChannel.SC_OUT_OFFSET), ISO7816.SW_NO_ERROR); 995 | } 996 | 997 | /** 998 | * Logically shifts the given short to the right. Used internally by the generateMnemonic method. This method exists 999 | * because a simple logical right shift using shorts would most likely work on the actual target (which does math on 1000 | * shorts) but not on the simulator since a negative short would first be extended to 32-bit, shifted and then cut 1001 | * back to 16-bit, doing the equivalent of an arithmetic shift. Simply masking by 0x0000FFFF before shifting is not an 1002 | * option because the code would not convert to CAP file (because of int usage). Since this method works on both 1003 | * JavaCard and simulator and it is not invoked very often, the performance hit is non-existent. 1004 | * 1005 | * @param v value to shift 1006 | * @param amount amount 1007 | * @return logically right shifted value 1008 | */ 1009 | private short logicrShift(short v, short amount) { 1010 | if (amount == 0) return v; // short circuit on 0 1011 | short tmp = (short) (v & 0x7fff); 1012 | 1013 | if (tmp == v) { 1014 | return (short) (v >>> amount); 1015 | } 1016 | 1017 | tmp = (short) (tmp >>> amount); 1018 | 1019 | return (short) ((short)((short) 0x4000 >>> (short) (amount - 1)) | tmp); 1020 | } 1021 | 1022 | /** 1023 | * Clear all keys and erases the key UID. 1024 | */ 1025 | private void clearKeys() { 1026 | keyPathLen = 0; 1027 | pinlessPathLen = 0; 1028 | tmpPathLen = 0; 1029 | isExtended = false; 1030 | masterPrivate.clearKey(); 1031 | masterPublic.clearKey(); 1032 | resetCurveParameters(); 1033 | Util.arrayFillNonAtomic(masterChainCode, (short) 0, (short) masterChainCode.length, (byte) 0); 1034 | Util.arrayFillNonAtomic(altChainCode, (short) 0, (short) altChainCode.length, (byte) 0); 1035 | Util.arrayFillNonAtomic(keyPath, (short) 0, (short) keyPath.length, (byte) 0); 1036 | Util.arrayFillNonAtomic(pinlessPath, (short) 0, (short) pinlessPath.length, (byte) 0); 1037 | Util.arrayFillNonAtomic(tmpPath, (short) 0, (short) tmpPath.length, (byte) 0); 1038 | Util.arrayFillNonAtomic(derivationOutput, (short) 0, (short) derivationOutput.length, (byte) 0); 1039 | Util.arrayFillNonAtomic(keyUID, (short) 0, (short) keyUID.length, (byte) 0); 1040 | } 1041 | 1042 | /** 1043 | * Processes the REMOVE KEY command. Removes the master key and all derived keys. Secure Channel and PIN 1044 | * authentication are required. 1045 | * 1046 | * @param apdu the JCRE-owned APDU object. 1047 | */ 1048 | private void removeKey(APDU apdu) { 1049 | byte[] apduBuffer = apdu.getBuffer(); 1050 | secureChannel.preprocessAPDU(apduBuffer); 1051 | 1052 | if (!pin.isValidated()) { 1053 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 1054 | } 1055 | 1056 | clearKeys(); 1057 | } 1058 | 1059 | private void factoryReset(APDU apdu) { 1060 | byte[] apduBuffer = apdu.getBuffer(); 1061 | 1062 | if ((apduBuffer[OFFSET_P1] != FACTORY_RESET_P1_MAGIC) || (apduBuffer[OFFSET_P2] != FACTORY_RESET_P2_MAGIC)) { 1063 | ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); 1064 | } 1065 | 1066 | clearKeys(); 1067 | pin = null; 1068 | mainPIN = null; 1069 | altPIN = null; 1070 | puk = null; 1071 | secureChannel = null; 1072 | crypto.random.generateData(uid, (short) 0, UID_LENGTH); 1073 | Util.arrayFillNonAtomic(data, (short) 0, (short) data.length, (byte) 0); 1074 | 1075 | if (JCSystem.isObjectDeletionSupported()) { 1076 | JCSystem.requestObjectDeletion(); 1077 | } 1078 | } 1079 | 1080 | /** 1081 | * Processes the GENERATE KEY command. Requires an open Secure Channel and PIN authentication. The generated keys are 1082 | * extended and can be used with key derivation. They are not however generated according to BIP39, which means they 1083 | * do not have a mnemonic associated. 1084 | * 1085 | * @param apdu the JCRE-owned APDU object. 1086 | */ 1087 | private void generateKey(APDU apdu) { 1088 | byte[] apduBuffer = apdu.getBuffer(); 1089 | secureChannel.preprocessAPDU(apduBuffer); 1090 | 1091 | if (!pin.isValidated()) { 1092 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 1093 | } 1094 | 1095 | apduBuffer[ISO7816.OFFSET_LC] = BIP39_SEED_SIZE; 1096 | crypto.random.generateData(apduBuffer, ISO7816.OFFSET_CDATA, BIP39_SEED_SIZE); 1097 | 1098 | loadSeed(apduBuffer); 1099 | secureChannel.respond(apdu, KEY_UID_LENGTH, ISO7816.SW_NO_ERROR); 1100 | } 1101 | 1102 | /** 1103 | * Processes the SIGN command. Requires a secure channel to open and either the PIN to be verified or the PIN-less key 1104 | * path to be the current key path. This command supports signing a precomputed 32-bytes hash. The signature is 1105 | * generated using the current keys, so if no keys are loaded the command does not work. The result of the execution 1106 | * is not the plain signature, but a TLV object containing the public key which must be used to verify the signature 1107 | * and the signature itself. The client should use this to calculate 'v' and format the signature according to the 1108 | * format required for the transaction to be correctly inserted in the blockchain. 1109 | * 1110 | * @param apdu the JCRE-owned APDU object. 1111 | */ 1112 | private void sign(APDU apdu) { 1113 | byte[] apduBuffer = apdu.getBuffer(); 1114 | boolean usePinless = false; 1115 | boolean makeCurrent = false; 1116 | byte derivationSource = (byte) (apduBuffer[OFFSET_P1] & DERIVE_P1_SOURCE_MASK); 1117 | 1118 | switch((byte) (apduBuffer[OFFSET_P1] & ~DERIVE_P1_SOURCE_MASK)) { 1119 | case SIGN_P1_CURRENT_KEY: 1120 | derivationSource = DERIVE_P1_SOURCE_CURRENT; 1121 | break; 1122 | case SIGN_P1_DERIVE: 1123 | break; 1124 | case SIGN_P1_DERIVE_AND_MAKE_CURRENT: 1125 | makeCurrent = true; 1126 | break; 1127 | case SIGN_P1_PINLESS: 1128 | usePinless = true; 1129 | derivationSource = DERIVE_P1_SOURCE_PINLESS; 1130 | break; 1131 | default: 1132 | ISOException.throwIt(ISO7816.SW_WRONG_P1P2); 1133 | return; 1134 | } 1135 | 1136 | short len; 1137 | 1138 | if (usePinless && !secureChannel.isOpen()) { 1139 | len = (short) (apduBuffer[ISO7816.OFFSET_LC] & (short) 0xff); 1140 | } else { 1141 | len = secureChannel.preprocessAPDU(apduBuffer); 1142 | } 1143 | 1144 | if (usePinless && pinlessPathLen == 0) { 1145 | ISOException.throwIt(SW_REFERENCED_DATA_NOT_FOUND); 1146 | } 1147 | 1148 | if (len < MessageDigest.LENGTH_SHA_256) { 1149 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 1150 | } 1151 | 1152 | short pathLen = (short) (len - MessageDigest.LENGTH_SHA_256); 1153 | updateDerivationPath(apduBuffer, MessageDigest.LENGTH_SHA_256, pathLen, derivationSource); 1154 | 1155 | if (!((pin.isValidated() || usePinless || isPinless()) && masterPrivate.isInitialized())) { 1156 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 1157 | } 1158 | 1159 | doDerive(apduBuffer, MessageDigest.LENGTH_SHA_256); 1160 | 1161 | apduBuffer[SecureChannel.SC_OUT_OFFSET] = TLV_SIGNATURE_TEMPLATE; 1162 | apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 3)] = TLV_PUB_KEY; 1163 | short outLen = apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 4)] = Crypto.KEY_PUB_SIZE; 1164 | 1165 | secp256k1.derivePublicKey(derivationOutput, (short) 0, apduBuffer, (short) (SecureChannel.SC_OUT_OFFSET + 5)); 1166 | 1167 | outLen += 5; 1168 | short sigOff = (short) (SecureChannel.SC_OUT_OFFSET + outLen); 1169 | 1170 | signature.init(secp256k1.tmpECPrivateKey, Signature.MODE_SIGN); 1171 | 1172 | outLen += signature.signPreComputedHash(apduBuffer, ISO7816.OFFSET_CDATA, MessageDigest.LENGTH_SHA_256, apduBuffer, sigOff); 1173 | outLen += crypto.fixS(apduBuffer, sigOff); 1174 | 1175 | apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 1)] = (byte) 0x81; 1176 | apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 2)] = (byte) (outLen - 3); 1177 | 1178 | if (makeCurrent) { 1179 | commitTmpPath(); 1180 | } 1181 | 1182 | if (secureChannel.isOpen()) { 1183 | secureChannel.respond(apdu, outLen, ISO7816.SW_NO_ERROR); 1184 | } else { 1185 | apdu.setOutgoingAndSend(SecureChannel.SC_OUT_OFFSET, outLen); 1186 | } 1187 | } 1188 | 1189 | /** 1190 | * Processes the SET PINLESS PATH command. Requires an open secure channel and the PIN to be verified. It does not 1191 | * require keys to be loaded or the current key path to be set at a specific value. The data is formatted in the same 1192 | * way as for DERIVE KEY. In case the sequence of integers is empty, the PIN-less path is simply unset, so the master 1193 | * key can never become PIN-less. 1194 | * 1195 | * @param apdu the JCRE-owned APDU object. 1196 | */ 1197 | private void setPinlessPath(APDU apdu) { 1198 | byte[] apduBuffer = apdu.getBuffer(); 1199 | short len = secureChannel.preprocessAPDU(apduBuffer); 1200 | 1201 | if (!pin.isValidated()) { 1202 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 1203 | } 1204 | 1205 | if (((short) (len % 4) != 0) || (len > pinlessPath.length)) { 1206 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 1207 | } 1208 | 1209 | JCSystem.beginTransaction(); 1210 | pinlessPathLen = len; 1211 | Util.arrayCopy(apduBuffer, ISO7816.OFFSET_CDATA, pinlessPath, (short) 0, len); 1212 | JCSystem.commitTransaction(); 1213 | } 1214 | 1215 | /** 1216 | * Processes the EXPORT KEY command. Requires an open secure channel and the PIN to be verified. 1217 | * 1218 | * @param apdu the JCRE-owned APDU object. 1219 | */ 1220 | private void exportKey(APDU apdu) { 1221 | byte[] apduBuffer = apdu.getBuffer(); 1222 | short dataLen = secureChannel.preprocessAPDU(apduBuffer); 1223 | 1224 | if (!pin.isValidated() || !masterPrivate.isInitialized()) { 1225 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 1226 | } 1227 | 1228 | boolean publicOnly; 1229 | boolean extendedPublic; 1230 | 1231 | switch (apduBuffer[OFFSET_P2]) { 1232 | case EXPORT_KEY_P2_PRIVATE_AND_PUBLIC: 1233 | publicOnly = false; 1234 | extendedPublic = false; 1235 | break; 1236 | case EXPORT_KEY_P2_PUBLIC_ONLY: 1237 | publicOnly = true; 1238 | extendedPublic = false; 1239 | break; 1240 | case EXPORT_KEY_P2_EXTENDED_PUBLIC: 1241 | publicOnly = true; 1242 | extendedPublic = true; 1243 | break; 1244 | default: 1245 | ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); 1246 | return; 1247 | } 1248 | 1249 | boolean makeCurrent = false; 1250 | byte derivationSource = (byte) (apduBuffer[OFFSET_P1] & DERIVE_P1_SOURCE_MASK); 1251 | 1252 | switch ((byte) (apduBuffer[OFFSET_P1] & ~DERIVE_P1_SOURCE_MASK)) { 1253 | case EXPORT_KEY_P1_CURRENT: 1254 | derivationSource = DERIVE_P1_SOURCE_CURRENT; 1255 | break; 1256 | case EXPORT_KEY_P1_DERIVE: 1257 | break; 1258 | case EXPORT_KEY_P1_DERIVE_AND_MAKE_CURRENT: 1259 | makeCurrent = true; 1260 | break; 1261 | default: 1262 | ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); 1263 | return; 1264 | } 1265 | 1266 | updateDerivationPath(apduBuffer, (short) 0, dataLen, derivationSource); 1267 | 1268 | boolean eip1581 = isEIP1581(); 1269 | 1270 | if (!(publicOnly || eip1581) || (extendedPublic && eip1581)) { 1271 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 1272 | } 1273 | 1274 | doDerive(apduBuffer, (short) 0); 1275 | 1276 | short off = SecureChannel.SC_OUT_OFFSET; 1277 | 1278 | apduBuffer[off++] = TLV_KEY_TEMPLATE; 1279 | off++; 1280 | 1281 | short len; 1282 | 1283 | if (publicOnly) { 1284 | apduBuffer[off++] = TLV_PUB_KEY; 1285 | off++; 1286 | len = secp256k1.derivePublicKey(derivationOutput, (short) 0, apduBuffer, off); 1287 | apduBuffer[(short) (off - 1)] = (byte) len; 1288 | off += len; 1289 | 1290 | if (extendedPublic) { 1291 | apduBuffer[off++] = TLV_CHAIN_CODE; 1292 | off++; 1293 | Util.arrayCopyNonAtomic(derivationOutput, Crypto.KEY_SECRET_SIZE, apduBuffer, off, CHAIN_CODE_SIZE); 1294 | len = CHAIN_CODE_SIZE; 1295 | apduBuffer[(short) (off - 1)] = (byte) len; 1296 | off += len; 1297 | } 1298 | } else { 1299 | apduBuffer[off++] = TLV_PRIV_KEY; 1300 | off++; 1301 | 1302 | Util.arrayCopyNonAtomic(derivationOutput, (short) 0, apduBuffer, off, Crypto.KEY_SECRET_SIZE); 1303 | len = Crypto.KEY_SECRET_SIZE; 1304 | 1305 | apduBuffer[(short) (off - 1)] = (byte) len; 1306 | off += len; 1307 | } 1308 | 1309 | len = (short) (off - SecureChannel.SC_OUT_OFFSET); 1310 | apduBuffer[(SecureChannel.SC_OUT_OFFSET + 1)] = (byte) (len - 2); 1311 | 1312 | if (makeCurrent) { 1313 | commitTmpPath(); 1314 | } 1315 | 1316 | secureChannel.respond(apdu, len, ISO7816.SW_NO_ERROR); 1317 | } 1318 | 1319 | /** 1320 | * Processes the GET DATA command. 1321 | * 1322 | * @param apdu the JCRE-owned APDU object. 1323 | */ 1324 | private void getData(APDU apdu) { 1325 | byte[] apduBuffer = apdu.getBuffer(); 1326 | 1327 | if (secureChannel.isOpen()) { 1328 | secureChannel.preprocessAPDU(apduBuffer); 1329 | } 1330 | 1331 | byte[] dst; 1332 | short outLen; 1333 | short off = (short) 1; 1334 | 1335 | switch (apduBuffer[OFFSET_P1]) { 1336 | case STORE_DATA_P1_PUBLIC: 1337 | dst = data; 1338 | outLen = Util.makeShort((byte) 0x00, dst[0]); 1339 | break; 1340 | case STORE_DATA_P1_NDEF: 1341 | dst = SharedMemory.ndefDataFile; 1342 | outLen = (short) (Util.makeShort(dst[0], dst[1]) + 2); 1343 | //TODO: support output segmentation for NDEF 1344 | //off = (short) ((short) apduBuffer[OFFSET_P2] * 4); 1345 | off = (short) 0; 1346 | if (outLen > SecureChannel.SC_MAX_PLAIN_LENGTH) { 1347 | outLen = SecureChannel.SC_MAX_PLAIN_LENGTH; 1348 | } 1349 | break; 1350 | case STORE_DATA_P1_CASH: 1351 | dst = SharedMemory.cashDataFile; 1352 | outLen = Util.makeShort((byte) 0x00, dst[0]); 1353 | break; 1354 | default: 1355 | ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); 1356 | return; 1357 | } 1358 | 1359 | Util.arrayCopyNonAtomic(dst, off, apduBuffer, SecureChannel.SC_OUT_OFFSET, outLen); 1360 | 1361 | if (secureChannel.isOpen()) { 1362 | secureChannel.respond(apdu, outLen, ISO7816.SW_NO_ERROR); 1363 | } else { 1364 | apdu.setOutgoingAndSend(SecureChannel.SC_OUT_OFFSET, outLen); 1365 | } 1366 | } 1367 | 1368 | /** 1369 | * Processes the STORE DATA command. Requires an open secure channel and the PIN to be verified. 1370 | * 1371 | * @param apdu the JCRE-owned APDU object. 1372 | */ 1373 | private void storeData(APDU apdu) { 1374 | byte[] apduBuffer = apdu.getBuffer(); 1375 | secureChannel.preprocessAPDU(apduBuffer); 1376 | 1377 | if (!pin.isValidated()) { 1378 | ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); 1379 | } 1380 | 1381 | byte[] dst; 1382 | short dataLen = Util.makeShort((byte) 0x00, apduBuffer[ISO7816.OFFSET_LC]); 1383 | short off = (short) 0; 1384 | short inOff = ISO7816.OFFSET_LC; 1385 | 1386 | switch (apduBuffer[OFFSET_P1]) { 1387 | case STORE_DATA_P1_PUBLIC: 1388 | dst = data; 1389 | dataLen++; 1390 | break; 1391 | case STORE_DATA_P1_NDEF: 1392 | dst = SharedMemory.ndefDataFile; 1393 | off = (short) ((short) apduBuffer[OFFSET_P2] * 4); 1394 | inOff = ISO7816.OFFSET_CDATA; 1395 | break; 1396 | case STORE_DATA_P1_CASH: 1397 | dst = SharedMemory.cashDataFile; 1398 | dataLen++; 1399 | break; 1400 | default: 1401 | ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); 1402 | return; 1403 | } 1404 | 1405 | if ((short) (dataLen + off) > (short) dst.length) { 1406 | ISOException.throwIt(ISO7816.SW_WRONG_DATA); 1407 | } 1408 | 1409 | JCSystem.beginTransaction(); 1410 | Util.arrayCopy(apduBuffer, inOff, dst, off, dataLen); 1411 | JCSystem.commitTransaction(); 1412 | } 1413 | 1414 | /** 1415 | * Utility method to verify if all the bytes in the buffer between off (included) and off + len (excluded) are digits. 1416 | * 1417 | * @param buffer the buffer 1418 | * @param off the offset to begin checking 1419 | * @param len the length of the data 1420 | * @return whether all checked bytes are digits or not 1421 | */ 1422 | private boolean allDigits(byte[] buffer, short off, short len) { 1423 | while(len > 0) { 1424 | len--; 1425 | 1426 | byte c = buffer[(short)(off+len)]; 1427 | 1428 | if (c < 0x30 || c > 0x39) { 1429 | return false; 1430 | } 1431 | } 1432 | 1433 | return true; 1434 | } 1435 | 1436 | /** 1437 | * Returns whether the current key path is the same as the one defined as PIN-less or not 1438 | * @return whether the current key path is the same as the one defined as PIN-less or not 1439 | */ 1440 | private boolean isPinless() { 1441 | return (pinlessPathLen > 0) && (pinlessPathLen == tmpPathLen) && (Util.arrayCompare(tmpPath, (short) 0, pinlessPath, (short) 0, tmpPathLen) == 0); 1442 | } 1443 | 1444 | private boolean isEIP1581() { 1445 | return (tmpPathLen >= (short)(((short) EIP_1581_PREFIX.length) + 8)) && (Util.arrayCompare(EIP_1581_PREFIX, (short) 0, tmpPath, (short) 0, (short) EIP_1581_PREFIX.length) == 0); 1446 | } 1447 | 1448 | /** 1449 | * Set curve parameters to cleared keys 1450 | */ 1451 | private void resetCurveParameters() { 1452 | SECP256k1.setCurveParameters(masterPublic); 1453 | SECP256k1.setCurveParameters(masterPrivate); 1454 | } 1455 | } 1456 | -------------------------------------------------------------------------------- /src/test/java/im/status/keycard/KeycardTest.java: -------------------------------------------------------------------------------- 1 | package im.status.keycard; 2 | 3 | import com.licel.jcardsim.smartcardio.CardSimulator; 4 | import com.licel.jcardsim.smartcardio.CardTerminalSimulator; 5 | import com.licel.jcardsim.utils.AIDUtil; 6 | import im.status.keycard.applet.*; 7 | import im.status.keycard.applet.Certificate; 8 | import im.status.keycard.desktop.PCSCCardChannel; 9 | import im.status.keycard.io.APDUCommand; 10 | import im.status.keycard.io.APDUResponse; 11 | import javacard.framework.AID; 12 | import org.bitcoinj.core.ECKey; 13 | import org.bitcoinj.crypto.ChildNumber; 14 | import org.bitcoinj.crypto.DeterministicKey; 15 | import org.bitcoinj.crypto.HDKeyDerivation; 16 | import org.bouncycastle.jce.ECNamedCurveTable; 17 | import org.bouncycastle.jce.spec.ECParameterSpec; 18 | import org.bouncycastle.jce.spec.ECPublicKeySpec; 19 | import org.bouncycastle.util.encoders.Hex; 20 | import org.junit.jupiter.api.*; 21 | import org.web3j.crypto.*; 22 | import org.web3j.protocol.Web3j; 23 | import org.web3j.protocol.core.DefaultBlockParameterName; 24 | import org.web3j.protocol.core.methods.request.RawTransaction; 25 | import org.web3j.protocol.core.methods.response.EthSendTransaction; 26 | import org.web3j.protocol.http.HttpService; 27 | import org.web3j.tx.Transfer; 28 | import org.web3j.utils.Convert; 29 | import org.web3j.utils.Numeric; 30 | 31 | import javax.smartcardio.*; 32 | import java.io.ByteArrayOutputStream; 33 | import java.lang.reflect.Constructor; 34 | import java.lang.reflect.Field; 35 | import java.lang.reflect.Method; 36 | import java.math.BigDecimal; 37 | import java.math.BigInteger; 38 | import java.nio.ByteBuffer; 39 | import java.nio.ByteOrder; 40 | import java.security.*; 41 | 42 | import org.bouncycastle.jce.interfaces.ECPublicKey; 43 | 44 | import java.util.Arrays; 45 | import java.util.HashSet; 46 | import java.util.Random; 47 | 48 | import static org.apache.commons.codec.digest.DigestUtils.sha256; 49 | import static org.junit.jupiter.api.Assertions.*; 50 | 51 | import apdu4j.pcsc.TerminalManager; 52 | 53 | @DisplayName("Test the Keycard Applet") 54 | public class KeycardTest { 55 | // Pairing key is KeycardTest 56 | private static CardTerminal cardTerminal; 57 | private static CardChannel apduChannel; 58 | private static im.status.keycard.io.CardChannel sdkChannel; 59 | private static CardSimulator simulator; 60 | private static KeyPair caKeyPair; 61 | 62 | private static byte[] sharedSecret; 63 | 64 | private TestSecureChannelSession secureChannel; 65 | private TestKeycardCommandSet cmdSet; 66 | 67 | private static final int TARGET_SIMULATOR = 0; 68 | private static final int TARGET_CARD = 1; 69 | 70 | private static final int TARGET; 71 | 72 | static { 73 | switch(System.getProperty("im.status.keycard.test.target", "card")) { 74 | case "simulator": 75 | TARGET = TARGET_SIMULATOR; 76 | break; 77 | case "card": 78 | TARGET = TARGET_CARD; 79 | break; 80 | default: 81 | throw new RuntimeException("Unknown target"); 82 | } 83 | } 84 | 85 | @BeforeAll 86 | static void initAll() throws Exception { 87 | switch(TARGET) { 88 | case TARGET_SIMULATOR: 89 | openSimulatorChannel(); 90 | break; 91 | case TARGET_CARD: 92 | openCardChannel(); 93 | break; 94 | default: 95 | throw new IllegalStateException("Unknown target"); 96 | } 97 | 98 | caKeyPair = Certificate.generateIdentKeyPair(); 99 | 100 | initIfNeeded(); 101 | } 102 | 103 | private static void initCapabilities(ApplicationInfo info) { 104 | HashSet capabilities = new HashSet<>(); 105 | 106 | if (info.hasSecureChannelCapability()) { 107 | capabilities.add("secureChannel"); 108 | } 109 | 110 | if (info.hasCredentialsManagementCapability()) { 111 | capabilities.add("credentialsManagement"); 112 | } 113 | 114 | if (info.hasKeyManagementCapability()) { 115 | capabilities.add("keyManagement"); 116 | } 117 | 118 | if (info.hasNDEFCapability()) { 119 | capabilities.add("ndef"); 120 | } 121 | 122 | if (info.hasFactoryResetCapability()) { 123 | capabilities.add("factoryReset"); 124 | } 125 | 126 | CapabilityCondition.availableCapabilities = capabilities; 127 | } 128 | 129 | private static void openSimulatorChannel() throws Exception { 130 | simulator = new CardSimulator(); 131 | 132 | // Install KeycardApplet 133 | AID aid = AIDUtil.create(Identifiers.KEYCARD_AID); 134 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 135 | bos.write(Identifiers.getKeycardInstanceAID().length); 136 | bos.write(Identifiers.getKeycardInstanceAID()); 137 | 138 | simulator.installApplet(aid, KeycardApplet.class, bos.toByteArray(), (short) 0, (byte) bos.size()); 139 | bos.reset(); 140 | 141 | // Install NDEFApplet 142 | aid = AIDUtil.create(Identifiers.NDEF_AID); 143 | bos.write(Identifiers.NDEF_INSTANCE_AID.length); 144 | bos.write(Identifiers.NDEF_INSTANCE_AID); 145 | bos.write(new byte[] {0x01, 0x00, 0x02, (byte) 0xC9, 0x00}); 146 | 147 | simulator.installApplet(aid, NDEFApplet.class, bos.toByteArray(), (short) 0, (byte) bos.size()); 148 | bos.reset(); 149 | 150 | // Install CashApplet 151 | aid = AIDUtil.create(Identifiers.CASH_AID); 152 | bos.write(Identifiers.CASH_INSTANCE_AID.length); 153 | bos.write(Identifiers.CASH_INSTANCE_AID); 154 | bos.write(new byte[] {0x01, 0x00, 0x02, (byte) 0xC9, 0x00}); 155 | 156 | simulator.installApplet(aid, CashApplet.class, bos.toByteArray(), (short) 0, (byte) bos.size()); 157 | bos.reset(); 158 | 159 | // Install CashApplet 160 | aid = AIDUtil.create(Identifiers.IDENT_AID); 161 | bos.write(Identifiers.IDENT_INSTANCE_AID.length); 162 | bos.write(Identifiers.IDENT_INSTANCE_AID); 163 | bos.write(new byte[] {0x01, 0x00, 0x02, (byte) 0xC9, 0x00}); 164 | 165 | simulator.installApplet(aid, IdentApplet.class, bos.toByteArray(), (short) 0, (byte) bos.size()); 166 | bos.reset(); 167 | 168 | cardTerminal = CardTerminalSimulator.terminal(simulator); 169 | 170 | openPCSCChannel(); 171 | } 172 | 173 | private static void openCardChannel() throws Exception { 174 | TerminalFactory tf = TerminalManager.getTerminalFactory(); 175 | 176 | for (CardTerminal t : tf.terminals().list()) { 177 | if (t.isCardPresent()) { 178 | cardTerminal = t; 179 | break; 180 | } 181 | } 182 | 183 | openPCSCChannel(); 184 | } 185 | 186 | private static void openPCSCChannel() throws Exception { 187 | Card apduCard = cardTerminal.connect("*"); 188 | apduChannel = apduCard.getBasicChannel(); 189 | sdkChannel = new PCSCCardChannel(apduChannel); 190 | } 191 | 192 | private static void initCard(KeycardCommandSet cmdSet) throws Exception { 193 | assertEquals(0x9000, cmdSet.init("000000", "024680", "012345678901", sharedSecret, (byte) 3, (byte) 5).getSw()); 194 | cmdSet.select().checkOK(); 195 | } 196 | 197 | private static void initIfNeeded() throws Exception { 198 | KeyPair identKeyPair = Certificate.generateIdentKeyPair(); 199 | Certificate cert = Certificate.createCertificate(caKeyPair, identKeyPair); 200 | IdentCommandSet idCmdSet = new IdentCommandSet(sdkChannel); 201 | idCmdSet.select().checkOK(); 202 | idCmdSet.storeData(cert.toStoreData()).checkOK(); 203 | 204 | KeycardCommandSet cmdSet = new KeycardCommandSet(sdkChannel); 205 | cmdSet.select().checkOK(); 206 | 207 | initCapabilities(cmdSet.getApplicationInfo()); 208 | 209 | sharedSecret = cmdSet.pairingPasswordToSecret(System.getProperty("im.status.keycard.test.pairing", "KeycardDefaultPairing")); 210 | 211 | if (!cmdSet.getApplicationInfo().isInitializedCard()) { 212 | initCard(cmdSet); 213 | initCapabilities(cmdSet.getApplicationInfo()); 214 | } 215 | } 216 | 217 | @BeforeEach 218 | void init() throws Exception { 219 | reset(); 220 | cmdSet = new TestKeycardCommandSet(sdkChannel); 221 | secureChannel = new TestSecureChannelSession(); 222 | cmdSet.setSecureChannel(secureChannel); 223 | cmdSet.select().checkOK(); 224 | 225 | if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { 226 | cmdSet.autoPair(sharedSecret); 227 | } 228 | } 229 | 230 | @AfterEach 231 | void tearDown() throws Exception { 232 | resetAndSelectAndOpenSC(); 233 | 234 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 235 | APDUResponse response = cmdSet.verifyPIN("000000"); 236 | assertEquals(0x9000, response.getSw()); 237 | } 238 | 239 | if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { 240 | cmdSet.autoUnpair(); 241 | } 242 | } 243 | 244 | @Test 245 | @DisplayName("SELECT command") 246 | void selectTest() throws Exception { 247 | APDUResponse response = cmdSet.select(); 248 | assertEquals(0x9000, response.getSw()); 249 | byte[] data = response.getData(); 250 | assertTrue(new ApplicationInfo(data).isInitializedCard()); 251 | } 252 | 253 | @Test 254 | @DisplayName("IDENT command") 255 | void identTest() throws Exception { 256 | APDUResponse response = cmdSet.identifyCard(new byte[33]); 257 | assertEquals(0x6a80, response.getSw()); 258 | 259 | byte[] challenge = new byte[32]; 260 | Random random = new Random(); 261 | byte[] expectedCaPub = ((ECPublicKey) caKeyPair.getPublic()).getQ().getEncoded(true); 262 | 263 | 264 | random.nextBytes(challenge); 265 | response = cmdSet.identifyCard(challenge); 266 | assertEquals(0x9000, response.getSw()); 267 | byte[] caPub = Certificate.verifyIdentity(challenge, response.getData()); 268 | assertArrayEquals(expectedCaPub, caPub); 269 | 270 | cmdSet.autoOpenSecureChannel(); 271 | 272 | random.nextBytes(challenge); 273 | response = cmdSet.identifyCard(challenge); 274 | assertEquals(0x9000, response.getSw()); 275 | caPub = Certificate.verifyIdentity(challenge, response.getData()); 276 | assertArrayEquals(expectedCaPub, caPub); 277 | 278 | random.nextBytes(challenge); 279 | CashCommandSet cashCmdSet = new CashCommandSet(sdkChannel); 280 | response = cashCmdSet.select(); 281 | assertEquals(0x9000, response.getSw()); 282 | response = cashCmdSet.identifyCard(challenge); 283 | assertEquals(0x9000, response.getSw()); 284 | caPub = Certificate.verifyIdentity(challenge, response.getData()); 285 | assertArrayEquals(expectedCaPub, caPub); 286 | } 287 | 288 | @Test 289 | @DisplayName("OPEN SECURE CHANNEL command") 290 | @Capabilities("secureChannel") 291 | void openSecureChannelTest() throws Exception { 292 | // Wrong P1 293 | APDUResponse response = cmdSet.openSecureChannel((byte)(secureChannel.getPairingIndex() + 1), new byte[65]); 294 | assertEquals(0x6A86, response.getSw()); 295 | 296 | // Wrong data 297 | response = cmdSet.openSecureChannel(secureChannel.getPairingIndex(), new byte[66]); 298 | assertEquals(0x6A80, response.getSw()); 299 | 300 | // Good case 301 | response = cmdSet.openSecureChannel(secureChannel.getPairingIndex(), secureChannel.getPublicKey()); 302 | assertEquals(0x9000, response.getSw()); 303 | assertEquals(SecureChannel.SC_SECRET_LENGTH + SecureChannel.SC_BLOCK_SIZE, response.getData().length); 304 | secureChannel.processOpenSecureChannelResponse(response); 305 | 306 | // Send command before MUTUALLY AUTHENTICATE 307 | secureChannel.reset(); 308 | response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_APPLICATION); 309 | assertEquals(0x6985, response.getSw()); 310 | 311 | // Perform mutual authentication 312 | secureChannel.setOpen(); 313 | response = cmdSet.mutuallyAuthenticate(); 314 | assertEquals(0x9000, response.getSw()); 315 | 316 | try { 317 | secureChannel.verifyMutuallyAuthenticateResponse(response); 318 | } catch (Exception e) { 319 | fail("invalid mutually authenticate response"); 320 | } 321 | 322 | // Verify that the channel is open 323 | response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_APPLICATION); 324 | assertEquals(0x9000, response.getSw()); 325 | } 326 | 327 | @Test 328 | @DisplayName("MUTUALLY AUTHENTICATE command") 329 | @Capabilities("secureChannel") 330 | void mutuallyAuthenticateTest() throws Exception { 331 | // Mutual authentication before opening a Secure Channel 332 | APDUResponse response = cmdSet.mutuallyAuthenticate(); 333 | assertEquals(0x6985, response.getSw()); 334 | 335 | response = cmdSet.openSecureChannel(secureChannel.getPairingIndex(), secureChannel.getPublicKey()); 336 | assertEquals(0x9000, response.getSw()); 337 | secureChannel.processOpenSecureChannelResponse(response); 338 | 339 | // Wrong data format 340 | response = cmdSet.mutuallyAuthenticate(new byte[31]); 341 | assertEquals(0x6982, response.getSw()); 342 | 343 | // Verify that after wrong authentication, the command does not work 344 | response = cmdSet.mutuallyAuthenticate(); 345 | assertEquals(0x6985, response.getSw()); 346 | 347 | // Wrong authentication data 348 | response = cmdSet.openSecureChannel(secureChannel.getPairingIndex(), secureChannel.getPublicKey()); 349 | assertEquals(0x9000, response.getSw()); 350 | secureChannel.processOpenSecureChannelResponse(response); 351 | APDUResponse resp2 = sdkChannel.send(new APDUCommand(0x80, SecureChannel.INS_MUTUALLY_AUTHENTICATE, 0, 0, new byte[48])); 352 | assertEquals(0x6982, resp2.getSw()); 353 | secureChannel.reset(); 354 | response = cmdSet.mutuallyAuthenticate(); 355 | assertEquals(0x6985, response.getSw()); 356 | 357 | // Good case 358 | cmdSet.autoOpenSecureChannel(); 359 | 360 | // MUTUALLY AUTHENTICATE has no effect on an already open secure channel 361 | response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_APPLICATION); 362 | assertEquals(0x9000, response.getSw()); 363 | 364 | response = cmdSet.mutuallyAuthenticate(); 365 | assertEquals(0x6985, response.getSw()); 366 | 367 | response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_APPLICATION); 368 | assertEquals(0x9000, response.getSw()); 369 | } 370 | 371 | @Test 372 | @DisplayName("PAIR command") 373 | @Capabilities("secureChannel") 374 | void pairTest() throws Exception { 375 | // Wrong data length 376 | APDUResponse response = cmdSet.pair(SecureChannel.PAIR_P1_FIRST_STEP, new byte[31]); 377 | assertEquals(0x6A80, response.getSw()); 378 | 379 | // Wrong P1 380 | response = cmdSet.pair(SecureChannel.PAIR_P1_LAST_STEP, new byte[32]); 381 | assertEquals(0x6A86, response.getSw()); 382 | 383 | // Wrong client cryptogram 384 | byte[] challenge = new byte[32]; 385 | Random random = new Random(); 386 | random.nextBytes(challenge); 387 | response = cmdSet.pair(SecureChannel.PAIR_P1_FIRST_STEP, challenge); 388 | assertEquals(0x9000, response.getSw()); 389 | response = cmdSet.pair(SecureChannel.PAIR_P1_LAST_STEP, challenge); 390 | assertEquals(0x6982, response.getSw()); 391 | 392 | // Interrupt session 393 | random.nextBytes(challenge); 394 | response = cmdSet.pair(SecureChannel.PAIR_P1_FIRST_STEP, challenge); 395 | assertEquals(0x9000, response.getSw()); 396 | cmdSet.openSecureChannel(secureChannel.getPairingIndex(), secureChannel.getPublicKey()); 397 | response = cmdSet.pair(SecureChannel.PAIR_P1_LAST_STEP, challenge); 398 | assertEquals(0x6A86, response.getSw()); 399 | 400 | // Open secure channel 401 | cmdSet.autoOpenSecureChannel(); 402 | response = cmdSet.pair(SecureChannel.PAIR_P1_FIRST_STEP, challenge); 403 | assertTrue((0x6985 == response.getSw()) || (0x6982 == response.getSw())); 404 | cmdSet.openSecureChannel(secureChannel.getPairingIndex(), secureChannel.getPublicKey()); 405 | 406 | // Pair multiple indexes 407 | for (int i = 1; i < KeycardApplet.PAIRING_MAX_CLIENT_COUNT; i++) { 408 | cmdSet.autoPair(sharedSecret); 409 | assertEquals(i, secureChannel.getPairingIndex()); 410 | cmdSet.autoOpenSecureChannel(); 411 | cmdSet.openSecureChannel(secureChannel.getPairingIndex(), secureChannel.getPublicKey()); 412 | } 413 | 414 | Pairing tmpPairing = cmdSet.getPairing(); 415 | 416 | // Too many paired indexes 417 | response = cmdSet.pair(SecureChannel.PAIR_P1_FIRST_STEP, SecureChannel.PAIR_P2_PERSISTENT, challenge); 418 | assertEquals(0x6A84, response.getSw()); 419 | 420 | // Ephemeral pairing 421 | cmdSet.autoPair(sharedSecret); 422 | assertEquals((byte) 0xff, secureChannel.getPairingIndex()); 423 | 424 | // Unpair all (except the last one, which will be unpaired in the tearDown phase) 425 | cmdSet.autoOpenSecureChannel(); 426 | 427 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 428 | response = cmdSet.verifyPIN("000000"); 429 | assertEquals(0x9000, response.getSw()); 430 | } 431 | 432 | for (byte i = 0; i < (KeycardApplet.PAIRING_MAX_CLIENT_COUNT - 1); i++) { 433 | response = cmdSet.unpair(i); 434 | assertEquals(0x9000, response.getSw()); 435 | } 436 | 437 | // ephemeral pairing is lost on reset, so we need to use a persistent one 438 | cmdSet.setPairing(tmpPairing); 439 | } 440 | 441 | @Test 442 | @DisplayName("UNPAIR command") 443 | @Capabilities("secureChannel") 444 | void unpairTest() throws Exception { 445 | // Add a spare keyset 446 | byte sparePairingIndex = secureChannel.getPairingIndex(); 447 | cmdSet.autoPair(sharedSecret); 448 | 449 | // Proof that the old keyset is still usable 450 | APDUResponse response = cmdSet.openSecureChannel(sparePairingIndex, secureChannel.getPublicKey()); 451 | assertEquals(0x9000, response.getSw()); 452 | 453 | // Security condition violation: SecureChannel not open 454 | response = cmdSet.unpair(sparePairingIndex); 455 | assertEquals(0x6985, response.getSw()); 456 | 457 | // Not authenticated 458 | cmdSet.autoOpenSecureChannel(); 459 | 460 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 461 | response = cmdSet.unpair(sparePairingIndex); 462 | assertEquals(0x6985, response.getSw()); 463 | 464 | response = cmdSet.verifyPIN("000000"); 465 | assertEquals(0x9000, response.getSw()); 466 | } 467 | 468 | // Wrong P1 469 | response = cmdSet.unpair(KeycardApplet.PAIRING_MAX_CLIENT_COUNT); 470 | assertEquals(0x6A86, response.getSw()); 471 | 472 | // Unpair spare keyset 473 | response = cmdSet.unpair(sparePairingIndex); 474 | assertEquals(0x9000, response.getSw()); 475 | 476 | // Proof that unpaired is not usable 477 | response = cmdSet.openSecureChannel(sparePairingIndex, secureChannel.getPublicKey()); 478 | assertEquals(0x6A86, response.getSw()); 479 | } 480 | 481 | @Test 482 | @DisplayName("GET STATUS command") 483 | void getStatusTest() throws Exception { 484 | APDUResponse response; 485 | 486 | if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { 487 | // Security condition violation: SecureChannel not open 488 | response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_APPLICATION); 489 | assertEquals(0x6985, response.getSw()); 490 | cmdSet.autoOpenSecureChannel(); 491 | } 492 | 493 | // Good case. Since the order of test execution is undefined, the test cannot know if the keys are initialized or not. 494 | // Additionally, support for public key derivation is hw dependent. 495 | response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_APPLICATION); 496 | assertEquals(0x9000, response.getSw()); 497 | ApplicationStatus status = new ApplicationStatus(response.getData()); 498 | 499 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 500 | assertEquals(3, status.getPINRetryCount()); 501 | assertEquals(5, status.getPUKRetryCount()); 502 | 503 | response = cmdSet.verifyPIN("123456"); 504 | assertEquals(0x63C2, response.getSw()); 505 | response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_APPLICATION); 506 | assertEquals(0x9000, response.getSw()); 507 | status = new ApplicationStatus(response.getData()); 508 | assertEquals(2, status.getPINRetryCount()); 509 | assertEquals(5, status.getPUKRetryCount()); 510 | 511 | response = cmdSet.verifyPIN("000000"); 512 | assertEquals(0x9000, response.getSw()); 513 | response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_APPLICATION); 514 | assertEquals(0x9000, response.getSw()); 515 | status = new ApplicationStatus(response.getData()); 516 | assertEquals(3, status.getPINRetryCount()); 517 | assertEquals(5, status.getPUKRetryCount()); 518 | } else { 519 | assertEquals((byte) 0xff, status.getPINRetryCount()); 520 | assertEquals((byte) 0xff, status.getPUKRetryCount()); 521 | } 522 | 523 | // Check that key path is valid 524 | response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_KEY_PATH); 525 | assertEquals(0x9000, response.getSw()); 526 | KeyPath path = new KeyPath(response.getData()); 527 | assertNotEquals(null, path); 528 | } 529 | 530 | @Test 531 | @DisplayName("VERIFY PIN command") 532 | @Capabilities("credentialsManagement") 533 | void verifyPinTest() throws Exception { 534 | // Security condition violation: SecureChannel not open 535 | APDUResponse response = cmdSet.verifyPIN("000000"); 536 | assertEquals(0x6985, response.getSw()); 537 | 538 | cmdSet.autoOpenSecureChannel(); 539 | 540 | // Wrong format 541 | response = cmdSet.verifyPIN("12345"); 542 | assertEquals(0x6a80, response.getSw()); 543 | 544 | response = cmdSet.verifyPIN("12345a"); 545 | assertEquals(0x6a80, response.getSw()); 546 | 547 | // Wrong PIN 548 | response = cmdSet.verifyPIN("123456"); 549 | assertEquals(0x63C2, response.getSw()); 550 | 551 | // Correct PIN 552 | response = cmdSet.verifyPIN("000000"); 553 | assertEquals(0x9000, response.getSw()); 554 | 555 | // Alt PIN 556 | response = cmdSet.verifyPIN("024680"); 557 | assertEquals(0x9000, response.getSw()); 558 | 559 | // Check max retry counter 560 | response = cmdSet.verifyPIN("123456"); 561 | assertEquals(0x63C2, response.getSw()); 562 | 563 | response = cmdSet.verifyPIN("123456"); 564 | assertEquals(0x63C1, response.getSw()); 565 | 566 | response = cmdSet.verifyPIN("123456"); 567 | assertEquals(0x63C0, response.getSw()); 568 | 569 | response = cmdSet.verifyPIN("000000"); 570 | assertEquals(0x63C0, response.getSw()); 571 | 572 | response = cmdSet.verifyPIN("024680"); 573 | assertEquals(0x63C0, response.getSw()); 574 | 575 | // Unblock PIN to make further tests possible 576 | response = cmdSet.unblockPIN("012345678901", "024680"); 577 | assertEquals(0x9000, response.getSw()); 578 | } 579 | 580 | @Test 581 | @DisplayName("CHANGE PIN command") 582 | @Capabilities("credentialsManagement") 583 | void changePinTest() throws Exception { 584 | // Security condition violation: SecureChannel not open 585 | APDUResponse response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "123456"); 586 | assertEquals(0x6985, response.getSw()); 587 | 588 | cmdSet.autoOpenSecureChannel(); 589 | 590 | // Security condition violation: PIN not verified 591 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "123456"); 592 | assertEquals(0x6985, response.getSw()); 593 | 594 | response = cmdSet.verifyPIN("000000"); 595 | assertEquals(0x9000, response.getSw()); 596 | 597 | // Wrong P1 598 | response = cmdSet.changePIN(0x03, "123456"); 599 | assertEquals(0x6a86, response.getSw()); 600 | 601 | // Test wrong PIN formats (non-digits, too short, too long) 602 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "654a21"); 603 | assertEquals(0x6A80, response.getSw()); 604 | 605 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "54321"); 606 | assertEquals(0x6A80, response.getSw()); 607 | 608 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "7654321"); 609 | assertEquals(0x6A80, response.getSw()); 610 | 611 | // Test wrong PUK formats 612 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PUK, "210987654a21"); 613 | assertEquals(0x6A80, response.getSw()); 614 | 615 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PUK, "10987654321"); 616 | assertEquals(0x6A80, response.getSw()); 617 | 618 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PUK, "3210987654321"); 619 | assertEquals(0x6A80, response.getSw()); 620 | 621 | // Test wrong pairing secret format (too long, too short) 622 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PAIRING_SECRET, "abcdefghilmnopqrstuvz123456789012"); 623 | assertEquals(0x6A80, response.getSw()); 624 | 625 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PAIRING_SECRET, "abcdefghilmnopqrstuvz1234567890"); 626 | assertEquals(0x6A80, response.getSw()); 627 | 628 | // Change PIN correctly, check that after PIN change the PIN remains validated 629 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "123456"); 630 | assertEquals(0x9000, response.getSw()); 631 | 632 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "654321"); 633 | assertEquals(0x9000, response.getSw()); 634 | 635 | // Reset card and verify that the new PIN has really been set 636 | resetAndSelectAndOpenSC(); 637 | 638 | response = cmdSet.verifyPIN("654321"); 639 | assertEquals(0x9000, response.getSw()); 640 | 641 | // Change PUK 642 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PUK, "210987654321"); 643 | assertEquals(0x9000, response.getSw()); 644 | 645 | resetAndSelectAndOpenSC(); 646 | 647 | response = cmdSet.verifyPIN("000000"); 648 | assertEquals(0x63C2, response.getSw()); 649 | response = cmdSet.verifyPIN("000000"); 650 | assertEquals(0x63C1, response.getSw()); 651 | response = cmdSet.verifyPIN("000000"); 652 | assertEquals(0x63C0, response.getSw()); 653 | 654 | // Reset the PIN with the new PUK 655 | response = cmdSet.unblockPIN("210987654321", "000000"); 656 | assertEquals(0x9000, response.getSw()); 657 | 658 | response = cmdSet.verifyPIN("000000"); 659 | assertEquals(0x9000, response.getSw()); 660 | 661 | // Reset PUK 662 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PUK, "012345678901"); 663 | assertEquals(0x9000, response.getSw()); 664 | 665 | // Change the pairing secret 666 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PAIRING_SECRET, "abcdefghilmnopqrstuvz12345678901"); 667 | assertEquals(0x9000, response.getSw()); 668 | cmdSet.autoUnpair(); 669 | reset(); 670 | response = cmdSet.select(); 671 | assertEquals(0x9000, response.getSw()); 672 | cmdSet.autoPair("abcdefghilmnopqrstuvz12345678901".getBytes()); 673 | 674 | // Reset pairing secret 675 | cmdSet.autoOpenSecureChannel(); 676 | 677 | response = cmdSet.verifyPIN("000000"); 678 | assertEquals(0x9000, response.getSw()); 679 | 680 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_PAIRING_SECRET, sharedSecret); 681 | assertEquals(0x9000, response.getSw()); 682 | 683 | // Alt PIN 684 | response = cmdSet.verifyPIN("024680"); 685 | assertEquals(0x9000, response.getSw()); 686 | 687 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "123456"); 688 | assertEquals(0x9000, response.getSw()); 689 | 690 | resetAndSelectAndOpenSC(); 691 | 692 | response = cmdSet.verifyPIN("123456"); 693 | assertEquals(0x9000, response.getSw()); 694 | 695 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "024680"); 696 | assertEquals(0x9000, response.getSw()); 697 | 698 | resetAndSelectAndOpenSC(); 699 | 700 | response = cmdSet.verifyPIN("000000"); 701 | assertEquals(0x9000, response.getSw()); 702 | } 703 | 704 | @Test 705 | @DisplayName("UNBLOCK PIN command") 706 | @Capabilities("credentialsManagement") 707 | void unblockPinTest() throws Exception { 708 | // Security condition violation: SecureChannel not open 709 | APDUResponse response = cmdSet.unblockPIN("012345678901", "000000"); 710 | assertEquals(0x6985, response.getSw()); 711 | 712 | cmdSet.autoOpenSecureChannel(); 713 | 714 | // Condition violation: PIN is not blocked 715 | response = cmdSet.unblockPIN("012345678901", "000000"); 716 | assertEquals(0x6985, response.getSw()); 717 | 718 | // Block the PIN 719 | response = cmdSet.verifyPIN("123456"); 720 | assertEquals(0x63C2, response.getSw()); 721 | 722 | response = cmdSet.verifyPIN("123456"); 723 | assertEquals(0x63C1, response.getSw()); 724 | 725 | response = cmdSet.verifyPIN("123456"); 726 | assertEquals(0x63C0, response.getSw()); 727 | 728 | // Wrong PUK formats (too short, too long) 729 | response = cmdSet.unblockPIN("12345678901", "000000"); 730 | assertEquals(0x6A80, response.getSw()); 731 | 732 | response = cmdSet.unblockPIN("1234567890123", "000000"); 733 | assertEquals(0x6A80, response.getSw()); 734 | 735 | // Wrong PUK 736 | response = cmdSet.unblockPIN("123456789010", "000000"); 737 | assertEquals(0x63C4, response.getSw()); 738 | 739 | // Correct PUK 740 | response = cmdSet.unblockPIN("012345678901", "654321"); 741 | assertEquals(0x9000, response.getSw()); 742 | 743 | // Check that PIN has been changed and unblocked 744 | resetAndSelectAndOpenSC(); 745 | 746 | response = cmdSet.verifyPIN("654321"); 747 | assertEquals(0x9000, response.getSw()); 748 | 749 | // Reset the PIN to make further tests possible 750 | response = cmdSet.changePIN(KeycardApplet.CHANGE_PIN_P1_USER_PIN, "000000"); 751 | assertEquals(0x9000, response.getSw()); 752 | } 753 | 754 | @Test 755 | @DisplayName("LOAD KEY command") 756 | @Capabilities("keyManagement") 757 | void loadKeyTest() throws Exception { 758 | KeyPairGenerator g = keypairGenerator(); 759 | KeyPair keyPair = g.generateKeyPair(); 760 | APDUResponse response; 761 | 762 | if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { 763 | // Security condition violation: SecureChannel not open 764 | response = cmdSet.loadKey(keyPair); 765 | assertEquals(0x6985, response.getSw()); 766 | 767 | cmdSet.autoOpenSecureChannel(); 768 | } 769 | 770 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 771 | // Security condition violation: PIN not verified 772 | response = cmdSet.loadKey(keyPair); 773 | assertEquals(0x6985, response.getSw()); 774 | 775 | response = cmdSet.verifyPIN("000000"); 776 | assertEquals(0x9000, response.getSw()); 777 | } 778 | 779 | // Wrong key type 780 | response = cmdSet.loadKey(new byte[] { (byte) 0xAA, 0x02, (byte) 0x80, 0x00}, (byte) 0x00); 781 | assertEquals(0x6A86, response.getSw()); 782 | 783 | // Wrong data (wrong template, missing private key, invalid keys) 784 | response = cmdSet.loadKey(new byte[]{(byte) 0xAA, 0x02, (byte) 0x80, 0x00}, KeycardApplet.LOAD_KEY_P1_EC); 785 | assertEquals(0x6A80, response.getSw()); 786 | 787 | response = cmdSet.loadKey(new byte[]{(byte) 0xA1, 0x02, (byte) 0x80, 0x00}, KeycardApplet.LOAD_KEY_P1_EC); 788 | assertEquals(0x6A80, response.getSw()); 789 | 790 | if (TARGET != TARGET_SIMULATOR) { // the simulator does not check the key format 791 | response = cmdSet.loadKey(new byte[]{(byte) 0xA1, 0x06, (byte) 0x80, 0x01, 0x01, (byte) 0x81, 0x01, 0x02}, KeycardApplet.LOAD_KEY_P1_EC); 792 | assertEquals(0x6A80, response.getSw()); 793 | } 794 | 795 | byte[] chainCode = new byte[32]; 796 | new Random().nextBytes(chainCode); 797 | 798 | // Correct LOAD KEY 799 | response = cmdSet.loadKey(keyPair); 800 | assertEquals(0x9000, response.getSw()); 801 | verifyKeyUID(response.getData(), ((ECPublicKey) keyPair.getPublic())); 802 | 803 | keyPair = g.generateKeyPair(); 804 | 805 | // Check extended key 806 | response = cmdSet.loadKey(keyPair, false, chainCode); 807 | assertEquals(0x9000, response.getSw()); 808 | verifyKeyUID(response.getData(), ((ECPublicKey) keyPair.getPublic())); 809 | 810 | // Check omitted public key 811 | response = cmdSet.loadKey(keyPair, true, null); 812 | assertEquals(0x9000, response.getSw()); 813 | verifyKeyUID(response.getData(), ((ECPublicKey) keyPair.getPublic())); 814 | response = cmdSet.loadKey(keyPair, true, chainCode); 815 | assertEquals(0x9000, response.getSw()); 816 | verifyKeyUID(response.getData(), ((ECPublicKey) keyPair.getPublic())); 817 | 818 | // Check seed load 819 | response = cmdSet.loadKey(keyPair.getPrivate(), chainCode); 820 | assertEquals(0x9000, response.getSw()); 821 | } 822 | 823 | @Test 824 | @DisplayName("GENERATE MNEMONIC command") 825 | @Capabilities("keyManagement") 826 | void generateMnemonicTest() throws Exception { 827 | // Security condition violation: SecureChannel not open 828 | APDUResponse response; 829 | 830 | if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { 831 | response = cmdSet.generateMnemonic(4); 832 | assertEquals(0x6985, response.getSw()); 833 | cmdSet.autoOpenSecureChannel(); 834 | } 835 | 836 | // Wrong P1 (too short, too long) 837 | response = cmdSet.generateMnemonic(3); 838 | assertEquals(0x6A86, response.getSw()); 839 | 840 | response = cmdSet.generateMnemonic(9); 841 | assertEquals(0x6A86, response.getSw()); 842 | 843 | // Good cases 844 | response = cmdSet.generateMnemonic(4); 845 | assertEquals(0x9000, response.getSw()); 846 | assertMnemonic(12, response.getData()); 847 | 848 | response = cmdSet.generateMnemonic(5); 849 | assertEquals(0x9000, response.getSw()); 850 | assertMnemonic(15, response.getData()); 851 | 852 | response = cmdSet.generateMnemonic(6); 853 | assertEquals(0x9000, response.getSw()); 854 | assertMnemonic(18, response.getData()); 855 | 856 | response = cmdSet.generateMnemonic(7); 857 | assertEquals(0x9000, response.getSw()); 858 | assertMnemonic(21, response.getData()); 859 | 860 | response = cmdSet.generateMnemonic(8); 861 | assertEquals(0x9000, response.getSw()); 862 | assertMnemonic(24, response.getData()); 863 | } 864 | 865 | @Test 866 | @DisplayName("REMOVE KEY command") 867 | @Capabilities("keyManagement") 868 | void removeKeyTest() throws Exception { 869 | KeyPairGenerator g = keypairGenerator(); 870 | KeyPair keyPair = g.generateKeyPair(); 871 | APDUResponse response; 872 | 873 | if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { 874 | // Security condition violation: SecureChannel not open 875 | response = cmdSet.removeKey(); 876 | assertEquals(0x6985, response.getSw()); 877 | cmdSet.autoOpenSecureChannel(); 878 | } 879 | 880 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 881 | // Security condition violation: PIN not verified 882 | response = cmdSet.removeKey(); 883 | assertEquals(0x6985, response.getSw()); 884 | 885 | response = cmdSet.verifyPIN("000000"); 886 | assertEquals(0x9000, response.getSw()); 887 | } 888 | 889 | response = cmdSet.loadKey(keyPair); 890 | assertEquals(0x9000, response.getSw()); 891 | 892 | response = cmdSet.select(); 893 | assertEquals(0x9000, response.getSw()); 894 | ApplicationInfo info = new ApplicationInfo(response.getData()); 895 | verifyKeyUID(info.getKeyUID(), (ECPublicKey) keyPair.getPublic()); 896 | 897 | if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { 898 | cmdSet.autoOpenSecureChannel(); 899 | } 900 | 901 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 902 | response = cmdSet.verifyPIN("000000"); 903 | assertEquals(0x9000, response.getSw()); 904 | } 905 | 906 | assertTrue(cmdSet.getKeyInitializationStatus()); 907 | 908 | // Good case 909 | response = cmdSet.removeKey(); 910 | assertEquals(0x9000, response.getSw()); 911 | 912 | assertFalse(cmdSet.getKeyInitializationStatus()); 913 | 914 | response = cmdSet.select(); 915 | assertEquals(0x9000, response.getSw()); 916 | info = new ApplicationInfo(response.getData()); 917 | assertEquals(0, info.getKeyUID().length); 918 | } 919 | 920 | @Test 921 | @DisplayName("FACTORY RESET command") 922 | @Capabilities("factoryReset") 923 | void factoryResetTest() throws Exception { 924 | KeyPairGenerator g = keypairGenerator(); 925 | KeyPair keyPair = g.generateKeyPair(); 926 | 927 | // Invalid P1 P2 928 | APDUResponse response = sdkChannel.send(new APDUCommand(0x80, KeycardApplet.INS_FACTORY_RESET, 0, 0, new byte[0])); 929 | assertEquals(0x6a86, response.getSw()); 930 | 931 | // Good case 932 | response = cmdSet.factoryReset(); 933 | assertEquals(0x9000, response.getSw()); 934 | 935 | response = cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_KEY_PATH); 936 | assertEquals(0x6d00, response.getSw()); 937 | 938 | response = cmdSet.select(); 939 | assertEquals(0x9000, response.getSw()); 940 | assertFalse(cmdSet.getApplicationInfo().isInitializedCard()); 941 | 942 | initCard(cmdSet); 943 | 944 | response = cmdSet.select(); 945 | assertEquals(0x9000, response.getSw()); 946 | 947 | if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { 948 | cmdSet.autoPair(sharedSecret); 949 | cmdSet.autoOpenSecureChannel(); 950 | } 951 | 952 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 953 | response = cmdSet.verifyPIN("000000"); 954 | assertEquals(0x9000, response.getSw()); 955 | } 956 | 957 | assertFalse(cmdSet.getKeyInitializationStatus()); 958 | } 959 | 960 | @Test 961 | @DisplayName("GENERATE KEY command") 962 | @Capabilities("keyManagement") 963 | void generateKeyTest() throws Exception { 964 | APDUResponse response; 965 | 966 | if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { 967 | // Security condition violation: SecureChannel not open 968 | response = cmdSet.generateKey(); 969 | assertEquals(0x6985, response.getSw()); 970 | cmdSet.autoOpenSecureChannel(); 971 | } 972 | 973 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 974 | // Security condition violation: PIN not verified 975 | response = cmdSet.generateKey(); 976 | assertEquals(0x6985, response.getSw()); 977 | 978 | response = cmdSet.verifyPIN("000000"); 979 | assertEquals(0x9000, response.getSw()); 980 | } 981 | 982 | // Good case 983 | response = cmdSet.generateKey(); 984 | assertEquals(0x9000, response.getSw()); 985 | byte[] keyUID = response.getData(); 986 | 987 | response = cmdSet.exportCurrentKey(true); 988 | assertEquals(0x9000, response.getSw()); 989 | byte[] pubKey = response.getData(); 990 | 991 | verifyKeyUID(keyUID, Arrays.copyOfRange(pubKey, 4, pubKey.length)); 992 | } 993 | 994 | @Test 995 | @DisplayName("DERIVE KEY command") 996 | void deriveKeyTest() throws Exception { 997 | APDUResponse response; 998 | 999 | if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { 1000 | // Security condition violation: SecureChannel not open 1001 | response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x00}); 1002 | assertEquals(0x6985, response.getSw()); 1003 | 1004 | cmdSet.autoOpenSecureChannel(); 1005 | } 1006 | 1007 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 1008 | // Security condition violation: PIN is not verified 1009 | response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x00}); 1010 | assertEquals(0x6985, response.getSw()); 1011 | 1012 | response = cmdSet.verifyPIN("000000"); 1013 | assertEquals(0x9000, response.getSw()); 1014 | } 1015 | 1016 | KeyPairGenerator g = keypairGenerator(); 1017 | KeyPair keyPair = g.generateKeyPair(); 1018 | byte[] chainCode = new byte[32]; 1019 | new Random().nextBytes(chainCode); 1020 | 1021 | if (cmdSet.getApplicationInfo().hasKeyManagementCapability()) { 1022 | // Condition violation: keyset is not extended 1023 | response = cmdSet.loadKey(keyPair); 1024 | assertEquals(0x9000, response.getSw()); 1025 | response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x00}); 1026 | assertEquals(0x6985, response.getSw()); 1027 | 1028 | response = cmdSet.loadKey(keyPair, false, chainCode); 1029 | assertEquals(0x9000, response.getSw()); 1030 | } 1031 | 1032 | // Wrong data format 1033 | response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00}); 1034 | assertEquals(0x6A80, response.getSw()); 1035 | response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x00, 0x00}); 1036 | assertEquals(0x6A80, response.getSw()); 1037 | 1038 | // Correct 1039 | response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x01}); 1040 | assertEquals(0x9000, response.getSw()); 1041 | verifyKeyDerivation(keyPair, chainCode, new int[]{1}); 1042 | 1043 | // 3 levels with hardened key 1044 | response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x01, (byte) 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}); 1045 | assertEquals(0x9000, response.getSw()); 1046 | verifyKeyDerivation(keyPair, chainCode, new int[]{1, 0x80000000, 2}); 1047 | 1048 | // From parent 1049 | response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x03}, KeycardApplet.DERIVE_P1_SOURCE_PARENT); 1050 | assertEquals(0x9000, response.getSw()); 1051 | verifyKeyDerivation(keyPair, chainCode, new int[]{1, 0x80000000, 3}); 1052 | 1053 | // Reset master key 1054 | response = cmdSet.deriveKey(new byte[0]); 1055 | assertEquals(0x9000, response.getSw()); 1056 | verifyKeyDerivation(keyPair, chainCode, new int[0]); 1057 | 1058 | // Try parent when none available 1059 | response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x03}, KeycardApplet.DERIVE_P1_SOURCE_PARENT); 1060 | assertEquals(0x6B00, response.getSw()); 1061 | 1062 | // 3 levels with hardened key using separate commands 1063 | response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x01}, KeycardApplet.DERIVE_P1_SOURCE_MASTER); 1064 | assertEquals(0x9000, response.getSw()); 1065 | response = cmdSet.deriveKey(new byte[]{(byte) 0x80, 0x00, 0x00, 0x00}, KeycardApplet.DERIVE_P1_SOURCE_CURRENT); 1066 | assertEquals(0x9000, response.getSw()); 1067 | response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x02}, KeycardApplet.DERIVE_P1_SOURCE_CURRENT); 1068 | assertEquals(0x9000, response.getSw()); 1069 | verifyKeyDerivation(keyPair, chainCode, new int[]{1, 0x80000000, 2}); 1070 | 1071 | // Reset master key 1072 | response = cmdSet.deriveKey(new byte[0]); 1073 | assertEquals(0x9000, response.getSw()); 1074 | verifyKeyDerivation(keyPair, chainCode, new int[0]); 1075 | } 1076 | 1077 | @Test 1078 | @DisplayName("SIGN command") 1079 | void signTest() throws Exception { 1080 | byte[] data = "some data to be hashed".getBytes(); 1081 | byte[] hash = sha256(data); 1082 | 1083 | APDUResponse response; 1084 | 1085 | if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { 1086 | // Security condition violation: SecureChannel not open 1087 | response = cmdSet.sign(hash); 1088 | assertEquals(0x6985, response.getSw()); 1089 | 1090 | cmdSet.autoOpenSecureChannel(); 1091 | } 1092 | 1093 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 1094 | // Security condition violation: PIN not verified 1095 | response = cmdSet.sign(hash); 1096 | assertEquals(0x6985, response.getSw()); 1097 | 1098 | response = cmdSet.verifyPIN("000000"); 1099 | assertEquals(0x9000, response.getSw()); 1100 | } 1101 | 1102 | if (!cmdSet.getApplicationInfo().hasMasterKey()) { 1103 | response = cmdSet.generateKey(); 1104 | assertEquals(0x9000, response.getSw()); 1105 | } 1106 | 1107 | // Wrong Data length 1108 | response = cmdSet.sign(data); 1109 | assertEquals(0x6A80, response.getSw()); 1110 | 1111 | // Correctly sign a precomputed hash 1112 | response = cmdSet.sign(hash); 1113 | verifySignResp(data, response); 1114 | 1115 | // Sign and derive 1116 | String currentPath = new KeyPath(cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_KEY_PATH).checkOK().getData()).toString(); 1117 | String updatedPath = new KeyPath(currentPath + "/2").toString(); 1118 | response = cmdSet.signWithPath(hash, updatedPath, false); 1119 | verifySignResp(data, response); 1120 | assertEquals(currentPath, new KeyPath(cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_KEY_PATH).checkOK().getData()).toString()); 1121 | response = cmdSet.signWithPath(hash, updatedPath, true); 1122 | verifySignResp(data, response); 1123 | assertEquals(updatedPath, new KeyPath(cmdSet.getStatus(KeycardCommandSet.GET_STATUS_P1_KEY_PATH).checkOK().getData()).toString()); 1124 | 1125 | // Sign with PINless 1126 | String pinlessPath = currentPath + "/3"; 1127 | response = cmdSet.setPinlessPath(pinlessPath); 1128 | assertEquals(0x9000, response.getSw()); 1129 | 1130 | // No secure channel or PIN auth 1131 | response = cmdSet.select(); 1132 | assertEquals(0x9000, response.getSw()); 1133 | 1134 | response = cmdSet.signPinless(hash); 1135 | verifySignResp(data, response); 1136 | 1137 | // With secure channel 1138 | if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { 1139 | cmdSet.autoOpenSecureChannel(); 1140 | response = cmdSet.signPinless(hash); 1141 | verifySignResp(data, response); 1142 | } 1143 | 1144 | // No pinless path 1145 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 1146 | response = cmdSet.verifyPIN("000000"); 1147 | assertEquals(0x9000, response.getSw()); 1148 | } 1149 | 1150 | response = cmdSet.resetPinlessPath(); 1151 | assertEquals(0x9000, response.getSw()); 1152 | 1153 | response = cmdSet.signPinless(hash); 1154 | assertEquals(0x6A88, response.getSw()); 1155 | 1156 | // Alt PIN 1157 | response = cmdSet.verifyPIN("024680"); 1158 | assertEquals(0x9000, response.getSw()); 1159 | 1160 | response = cmdSet.signWithPath(hash, updatedPath, false); 1161 | verifySignResp(data, response); 1162 | } 1163 | 1164 | private void verifySignResp(byte[] data, APDUResponse response) throws Exception { 1165 | Signature signature = Signature.getInstance("SHA256withECDSA", "BC"); 1166 | assertEquals(0x9000, response.getSw()); 1167 | byte[] sig = response.getData(); 1168 | byte[] keyData = extractPublicKeyFromSignature(sig); 1169 | sig = extractSignature(sig); 1170 | 1171 | ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1"); 1172 | ECPublicKeySpec cardKeySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(keyData), ecSpec); 1173 | ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA", "BC").generatePublic(cardKeySpec); 1174 | 1175 | signature.initVerify(cardKey); 1176 | assertEquals((SecureChannel.SC_KEY_LENGTH * 2 / 8) + 1, keyData.length); 1177 | signature.update(data); 1178 | assertTrue(signature.verify(sig)); 1179 | assertFalse(isMalleable(sig)); 1180 | } 1181 | 1182 | @Test 1183 | @DisplayName("SET PINLESS PATH command") 1184 | @Capabilities("credentialsManagement") // The current test is not adapted to run automatically on devices without credentials management, since the tester must know what button to press 1185 | void setPinlessPathTest() throws Exception { 1186 | byte[] data = "some data to be hashed".getBytes(); 1187 | byte[] hash = sha256(data); 1188 | 1189 | KeyPairGenerator g = keypairGenerator(); 1190 | KeyPair keyPair = g.generateKeyPair(); 1191 | byte[] chainCode = new byte[32]; 1192 | new Random().nextBytes(chainCode); 1193 | 1194 | APDUResponse response; 1195 | 1196 | if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { 1197 | // Security condition violation: SecureChannel not open 1198 | response = cmdSet.setPinlessPath(new byte[]{0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}); 1199 | assertEquals(0x6985, response.getSw()); 1200 | 1201 | cmdSet.autoOpenSecureChannel(); 1202 | } 1203 | 1204 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 1205 | // Security condition violation: PIN not verified 1206 | response = cmdSet.setPinlessPath(new byte[]{0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}); 1207 | assertEquals(0x6985, response.getSw()); 1208 | 1209 | response = cmdSet.verifyPIN("000000"); 1210 | assertEquals(0x9000, response.getSw()); 1211 | } 1212 | 1213 | if (!cmdSet.getApplicationInfo().hasMasterKey()) { 1214 | response = cmdSet.loadKey(keyPair, false, chainCode); 1215 | assertEquals(0x9000, response.getSw()); 1216 | } 1217 | 1218 | // Wrong data 1219 | response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00}); 1220 | assertEquals(0x6a80, response.getSw()); 1221 | response = cmdSet.setPinlessPath(new byte[(KeycardApplet.KEY_PATH_MAX_DEPTH + 1)* 4]); 1222 | assertEquals(0x6a80, response.getSw()); 1223 | 1224 | // Correct 1225 | response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02}); 1226 | assertEquals(0x9000, response.getSw()); 1227 | 1228 | // Verify that only PINless path can be used without PIN 1229 | resetAndSelectAndOpenSC(); 1230 | response = cmdSet.sign(hash); 1231 | assertEquals(0x6985, response.getSw()); 1232 | 1233 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 1234 | response = cmdSet.verifyPIN("000000"); 1235 | assertEquals(0x9000, response.getSw()); 1236 | } 1237 | 1238 | response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01}, KeycardApplet.DERIVE_P1_SOURCE_MASTER); 1239 | assertEquals(0x9000, response.getSw()); 1240 | response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, KeycardApplet.DERIVE_P1_SOURCE_CURRENT); 1241 | assertEquals(0x9000, response.getSw()); 1242 | 1243 | resetAndSelectAndOpenSC(); 1244 | 1245 | response = cmdSet.sign(hash); 1246 | assertEquals(0x9000, response.getSw()); 1247 | 1248 | // Verify changing path 1249 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 1250 | response = cmdSet.verifyPIN("000000"); 1251 | assertEquals(0x9000, response.getSw()); 1252 | } 1253 | 1254 | response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01}); 1255 | assertEquals(0x9000, response.getSw()); 1256 | resetAndSelectAndOpenSC(); 1257 | response = cmdSet.sign(hash); 1258 | assertEquals(0x6985, response.getSw()); 1259 | 1260 | 1261 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 1262 | response = cmdSet.verifyPIN("000000"); 1263 | assertEquals(0x9000, response.getSw()); 1264 | } 1265 | 1266 | response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01}, KeycardApplet.DERIVE_P1_SOURCE_MASTER); 1267 | assertEquals(0x9000, response.getSw()); 1268 | resetAndSelectAndOpenSC(); 1269 | response = cmdSet.sign(hash); 1270 | assertEquals(0x9000, response.getSw()); 1271 | 1272 | // Reset 1273 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 1274 | response = cmdSet.verifyPIN("000000"); 1275 | assertEquals(0x9000, response.getSw()); 1276 | } 1277 | 1278 | response = cmdSet.setPinlessPath(new byte[] {}); 1279 | assertEquals(0x9000, response.getSw()); 1280 | resetAndSelectAndOpenSC(); 1281 | response = cmdSet.sign(hash); 1282 | assertEquals(0x6985, response.getSw()); 1283 | 1284 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 1285 | response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x02}, KeycardApplet.DERIVE_P1_SOURCE_MASTER); 1286 | assertEquals(0x6985, response.getSw()); 1287 | } 1288 | } 1289 | 1290 | @Test 1291 | @DisplayName("EXPORT KEY command") 1292 | void exportKey() throws Exception { 1293 | KeyPairGenerator g = keypairGenerator(); 1294 | KeyPair keyPair = g.generateKeyPair(); 1295 | byte[] chainCode = new byte[32]; 1296 | new Random().nextBytes(chainCode); 1297 | 1298 | APDUResponse response; 1299 | 1300 | if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { 1301 | // Security condition violation: SecureChannel not open 1302 | response = cmdSet.exportCurrentKey(true); 1303 | assertEquals(0x6985, response.getSw()); 1304 | 1305 | cmdSet.autoOpenSecureChannel(); 1306 | } 1307 | 1308 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 1309 | // Security condition violation: PIN not verified 1310 | response = cmdSet.exportCurrentKey(true); 1311 | assertEquals(0x6985, response.getSw()); 1312 | 1313 | response = cmdSet.verifyPIN("000000"); 1314 | assertEquals(0x9000, response.getSw()); 1315 | } 1316 | 1317 | if (cmdSet.getApplicationInfo().hasKeyManagementCapability()) { 1318 | response = cmdSet.loadKey(keyPair, false, chainCode); 1319 | assertEquals(0x9000, response.getSw()); 1320 | } 1321 | 1322 | response = cmdSet.deriveKey(new byte[0], KeycardApplet.DERIVE_P1_SOURCE_MASTER); 1323 | assertEquals(0x9000, response.getSw()); 1324 | 1325 | // Security condition violation: current key is not exportable 1326 | response = cmdSet.exportCurrentKey(false); 1327 | assertEquals(0x6985, response.getSw()); 1328 | 1329 | response = cmdSet.deriveKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2c, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00}, KeycardApplet.DERIVE_P1_SOURCE_MASTER); 1330 | assertEquals(0x9000, response.getSw()); 1331 | response = cmdSet.exportCurrentKey(false); 1332 | assertEquals(0x6985, response.getSw()); 1333 | 1334 | response = cmdSet.deriveKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2D, (byte) 0x00, 0x00, 0x00, 0x00}, KeycardApplet.DERIVE_P1_SOURCE_MASTER); 1335 | assertEquals(0x9000, response.getSw()); 1336 | response = cmdSet.exportCurrentKey(false); 1337 | assertEquals(0x6985, response.getSw()); 1338 | 1339 | // Export current public key 1340 | response = cmdSet.exportCurrentKey(true); 1341 | assertEquals(0x9000, response.getSw()); 1342 | byte[] keyTemplate = response.getData(); 1343 | verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002b, 0x8000003c, 0x8000062d, 0x00000000 }, true, false); 1344 | 1345 | // Derive & Make current 1346 | response = cmdSet.exportKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2D, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00}, KeycardApplet.DERIVE_P1_SOURCE_MASTER, true, false); 1347 | assertEquals(0x9000, response.getSw()); 1348 | keyTemplate = response.getData(); 1349 | verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002b, 0x8000003c, 0x8000062d, 0x00000000, 0x00000000 }, false, false); 1350 | 1351 | // Derive without making current 1352 | response = cmdSet.exportKey(new byte[] {(byte) 0x00, 0x00, 0x00, 0x01}, KeycardApplet.DERIVE_P1_SOURCE_PARENT, false,false); 1353 | assertEquals(0x9000, response.getSw()); 1354 | keyTemplate = response.getData(); 1355 | verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002b, 0x8000003c, 0x8000062d, 0x00000000, 0x00000001 }, false, false); 1356 | response = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_KEY_PATH); 1357 | assertEquals(0x9000, response.getSw()); 1358 | assertArrayEquals(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2D, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00}, response.getData()); 1359 | 1360 | // Export current 1361 | response = cmdSet.exportCurrentKey(false); 1362 | assertEquals(0x9000, response.getSw()); 1363 | keyTemplate = response.getData(); 1364 | verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002b, 0x8000003c, 0x8000062d, 0x00000000, 0x00000000 }, false, false); 1365 | 1366 | // Export extended public 1367 | response = cmdSet.exportKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2D, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00}, KeycardApplet.DERIVE_P1_SOURCE_MASTER, false, KeycardCommandSet.EXPORT_KEY_P2_EXTENDED_PUBLIC); 1368 | assertEquals(0x6985, response.getSw()); 1369 | 1370 | response = cmdSet.exportKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2c, (byte) 0x00, 0x00, 0x00, 0x00}, KeycardApplet.DERIVE_P1_SOURCE_MASTER, false, KeycardCommandSet.EXPORT_KEY_P2_EXTENDED_PUBLIC); 1371 | assertEquals(0x9000, response.getSw()); 1372 | keyTemplate = response.getData(); 1373 | verifyExportedKey(keyTemplate, keyPair, chainCode, new int[] { 0x8000002b, 0x8000003c, 0x8000062c, 0x00000000 }, true, true); 1374 | 1375 | // Reset 1376 | response = cmdSet.deriveKey(new byte[0], KeycardApplet.DERIVE_P1_SOURCE_MASTER); 1377 | assertEquals(0x9000, response.getSw()); 1378 | 1379 | // Alt PIN 1380 | response = cmdSet.verifyPIN("024680"); 1381 | assertEquals(0x9000, response.getSw()); 1382 | 1383 | response = cmdSet.exportKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x2B, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x06, 0x2c, (byte) 0x00, 0x00, 0x00, 0x00}, KeycardApplet.DERIVE_P1_SOURCE_MASTER, false, KeycardCommandSet.EXPORT_KEY_P2_EXTENDED_PUBLIC); 1384 | assertEquals(0x9000, response.getSw()); 1385 | keyTemplate = response.getData(); 1386 | verifyExportedKey(keyTemplate, keyPair, sha256(chainCode), new int[] { 0x8000002b, 0x8000003c, 0x8000062c, 0x00000000 }, true, true); 1387 | 1388 | } 1389 | 1390 | @Test 1391 | @DisplayName("STORE/GET DATA") 1392 | void storeGetDataTest() throws Exception { 1393 | APDUResponse response; 1394 | 1395 | if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { 1396 | // Security condition violation: SecureChannel not open 1397 | response = cmdSet.storeData(new byte[20], KeycardCommandSet.STORE_DATA_P1_PUBLIC); 1398 | assertEquals(0x6985, response.getSw()); 1399 | 1400 | cmdSet.autoOpenSecureChannel(); 1401 | } 1402 | 1403 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 1404 | // Security condition violation: PIN not verified 1405 | response = cmdSet.storeData(new byte[20], KeycardCommandSet.STORE_DATA_P1_PUBLIC); 1406 | assertEquals(0x6985, response.getSw()); 1407 | 1408 | response = cmdSet.verifyPIN("000000"); 1409 | assertEquals(0x9000, response.getSw()); 1410 | } 1411 | 1412 | // Data too long 1413 | response = cmdSet.storeData(new byte[128], KeycardCommandSet.STORE_DATA_P1_PUBLIC); 1414 | assertEquals(0x6A80, response.getSw()); 1415 | 1416 | byte[] data = new byte[127]; 1417 | 1418 | for (int i = 0; i < 127; i++) { 1419 | data[i] = (byte) i; 1420 | } 1421 | 1422 | // Correct data 1423 | response = cmdSet.storeData(data, KeycardCommandSet.STORE_DATA_P1_PUBLIC); 1424 | 1425 | assertEquals(0x9000, response.getSw()); 1426 | 1427 | // Read data back with secure channel 1428 | response = cmdSet.getData(KeycardCommandSet.STORE_DATA_P1_PUBLIC); 1429 | assertEquals(0x9000, response.getSw()); 1430 | assertArrayEquals(data, response.getData()); 1431 | 1432 | // Empty data 1433 | response = cmdSet.storeData(new byte[0], KeycardCommandSet.STORE_DATA_P1_PUBLIC); 1434 | assertEquals(0x9000, response.getSw()); 1435 | 1436 | response = cmdSet.getData(KeycardCommandSet.STORE_DATA_P1_PUBLIC); 1437 | assertEquals(0x9000, response.getSw()); 1438 | assertEquals(0, response.getData().length); 1439 | 1440 | // Shorter data 1441 | data = Arrays.copyOf(data, 20); 1442 | response = cmdSet.storeData(data, KeycardCommandSet.STORE_DATA_P1_PUBLIC); 1443 | assertEquals(0x9000, response.getSw()); 1444 | 1445 | // GET DATA without Secure Channel 1446 | cmdSet.select().checkOK(); 1447 | 1448 | response = cmdSet.getData(KeycardCommandSet.STORE_DATA_P1_PUBLIC); 1449 | assertEquals(0x9000, response.getSw()); 1450 | assertArrayEquals(data, response.getData()); 1451 | 1452 | if (cmdSet.getApplicationInfo().hasNDEFCapability()) { 1453 | byte[] ndefData = { 1454 | (byte) 0x00, (byte) 0x24, (byte) 0xd4, (byte) 0x0f, (byte) 0x12, (byte) 0x61, (byte) 0x6e, (byte) 0x64, 1455 | (byte) 0x72, (byte) 0x6f, (byte) 0x69, (byte) 0x64, (byte) 0x2e, (byte) 0x63, (byte) 0x6f, (byte) 0x6d, 1456 | (byte) 0x3a, (byte) 0x70, (byte) 0x6b, (byte) 0x67, (byte) 0x69, (byte) 0x6d, (byte) 0x2e, (byte) 0x73, 1457 | (byte) 0x74, (byte) 0x61, (byte) 0x74, (byte) 0x75, (byte) 0x73, (byte) 0x2e, (byte) 0x65, (byte) 0x74, 1458 | (byte) 0x68, (byte) 0x65, (byte) 0x72, (byte) 0x65, (byte) 0x75, (byte) 0x6d 1459 | }; 1460 | 1461 | // Security condition violation: SecureChannel not open 1462 | response = cmdSet.setNDEF(ndefData); 1463 | assertEquals(0x6985, response.getSw()); 1464 | 1465 | cmdSet.autoOpenSecureChannel(); 1466 | 1467 | // Security condition violation: PIN not verified 1468 | response = cmdSet.setNDEF(ndefData); 1469 | assertEquals(0x6985, response.getSw()); 1470 | 1471 | response = cmdSet.verifyPIN("000000"); 1472 | assertEquals(0x9000, response.getSw()); 1473 | 1474 | // Good case. 1475 | response = cmdSet.setNDEF(ndefData); 1476 | assertEquals(0x9000, response.getSw()); 1477 | 1478 | // Good case with no length. 1479 | response = cmdSet.setNDEF(Arrays.copyOfRange(ndefData, 2, ndefData.length)); 1480 | assertEquals(0x9000, response.getSw()); 1481 | 1482 | // Long message with segmentation 1483 | response = cmdSet.setNDEF(Hex.decode("c101000001b45402656e5468697320697320612072656c61746976656c79206c6f6e672074657874207265636f72642074686174206669747320696e2061626f75742035303020627974657320736f207468617420492063616e2074657374207365676d656e746174696f6e206f66207265636f726473207573696e6720746865206e657720616e6420696d70726f766564204e444546206170706c65742e20546869732069732071756974652061206c6f6e67207465787420746f2077726974652062757420686579204920616d206865726520666f7220746869732e204920776f6e277420636f707920616e64207061737465207468652073616d6520737472696e67206f76657220616e64206f766572206265636175736520492077616e7420746f206d616b6520737572652064617461206973207265616420636f72726563746c7920616e6420746865726520617265206e6f20737472616e67652073746974636865732e2049276420616c736f206c696b6520746f206d616b6520737572652065766572797468696e6720697320617320636c6f736520746f207265616c20776f726c6420757361676520617320706f737369626c65")); 1484 | assertEquals(0x9000, response.getSw()); 1485 | } 1486 | 1487 | data[0] = (byte) 0xAA; 1488 | 1489 | response = cmdSet.storeData(data, KeycardCommandSet.STORE_DATA_P1_CASH); 1490 | assertEquals(0x9000, response.getSw()); 1491 | 1492 | CashCommandSet cashCmdSet = new CashCommandSet(sdkChannel); 1493 | response = cashCmdSet.select(); 1494 | assertEquals(0x9000, response.getSw()); 1495 | CashApplicationInfo info = new CashApplicationInfo(response.getData()); 1496 | assertArrayEquals(data, info.getPubData()); 1497 | } 1498 | 1499 | @Test 1500 | @DisplayName("Test the Cash applet") 1501 | void cashTest() throws Exception { 1502 | CashCommandSet cashCmdSet = new CashCommandSet(sdkChannel); 1503 | APDUResponse response = cashCmdSet.select(); 1504 | assertEquals(0x9000, response.getSw()); 1505 | 1506 | CashApplicationInfo info = new CashApplicationInfo(response.getData()); 1507 | assertTrue(info.getAppVersion() > 0); 1508 | 1509 | byte[] data = "some data to be hashed".getBytes(); 1510 | byte[] hash = sha256(data); 1511 | 1512 | response = cashCmdSet.sign(hash); 1513 | verifySignResp(data, response); 1514 | } 1515 | 1516 | @Test 1517 | @DisplayName("Mnemonic load and derivation") 1518 | @Tag("manual") 1519 | void mnemonicTest() throws Exception { 1520 | if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { 1521 | cmdSet.autoOpenSecureChannel(); 1522 | } 1523 | 1524 | APDUResponse response; 1525 | 1526 | if (cmdSet.getApplicationInfo().hasCredentialsManagementCapability()) { 1527 | response = cmdSet.verifyPIN("000000"); 1528 | assertEquals(0x9000, response.getSw()); 1529 | } 1530 | 1531 | byte[] seed = Mnemonic.toBinarySeed("legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will", ""); 1532 | response = cmdSet.loadKey(seed); 1533 | assertEquals(0x9000, response.getSw()); 1534 | 1535 | response = cmdSet.exportCurrentKey(true); 1536 | assertEquals(0x9000, response.getSw()); 1537 | 1538 | BIP32KeyPair pubKey = BIP32KeyPair.fromTLV(response.getData()); 1539 | assertEquals("04cc620f846055ed43995391ca5e490c52251ea40453f64a0515bef84c24a653a7c4e02b9de56f66d9ee58dc6b591b534f5a20c0550b2c33a086b90b866cf70799", Hex.toHexString(pubKey.getPublicKey())); 1540 | 1541 | response = cmdSet.exportKey("m/43'/60'/1581'/0'/0", false, true); 1542 | assertEquals(0x9000, response.getSw()); 1543 | 1544 | pubKey = BIP32KeyPair.fromTLV(response.getData()); 1545 | assertEquals("04e7370d118461e1ab01f3e86e88c4b0c7b92cecb79c5e320cef73dda912f173beae74df15090b6405a274963c054cdfe6ac7843a302c260390d1fe776008f310e", Hex.toHexString(pubKey.getPublicKey())); 1546 | } 1547 | 1548 | @Test 1549 | @DisplayName("Sign actual Ethereum transaction") 1550 | @Tag("manual") 1551 | void signTransactionTest() throws Exception { 1552 | // Initialize credentials 1553 | Web3j web3j = Web3j.build(new HttpService()); 1554 | Credentials wallet1 = WalletUtils.loadCredentials("testwallet", "testwallets/wallet1.json"); 1555 | Credentials wallet2 = WalletUtils.loadCredentials("testwallet", "testwallets/wallet2.json"); 1556 | 1557 | // Load keys on card 1558 | cmdSet.autoOpenSecureChannel(); 1559 | APDUResponse response = cmdSet.verifyPIN("000000"); 1560 | assertEquals(0x9000, response.getSw()); 1561 | response = cmdSet.loadKey(wallet1.getEcKeyPair()); 1562 | assertEquals(0x9000, response.getSw()); 1563 | 1564 | // Verify balance 1565 | System.out.println("Wallet 1 balance: " + web3j.ethGetBalance(wallet1.getAddress(), DefaultBlockParameterName.LATEST).send().getBalance()); 1566 | System.out.println("Wallet 2 balance: " + web3j.ethGetBalance(wallet2.getAddress(), DefaultBlockParameterName.LATEST).send().getBalance()); 1567 | 1568 | // Create transaction 1569 | BigInteger gasPrice = web3j.ethGasPrice().send().getGasPrice(); 1570 | BigInteger weiValue = Convert.toWei(BigDecimal.valueOf(1.0), Convert.Unit.FINNEY).toBigIntegerExact(); 1571 | BigInteger nonce = web3j.ethGetTransactionCount(wallet1.getAddress(), DefaultBlockParameterName.LATEST).send().getTransactionCount(); 1572 | 1573 | RawTransaction rawTransaction = RawTransaction.createEtherTransaction(nonce, gasPrice, Transfer.GAS_LIMIT, wallet2.getAddress(), weiValue); 1574 | 1575 | // Sign transaction 1576 | byte[] txBytes = TransactionEncoder.encode(rawTransaction); 1577 | Sign.SignatureData signature = signMessage(txBytes); 1578 | 1579 | Method encode = TransactionEncoder.class.getDeclaredMethod("encode", RawTransaction.class, Sign.SignatureData.class); 1580 | encode.setAccessible(true); 1581 | 1582 | // Send transaction 1583 | byte[] signedMessage = (byte[]) encode.invoke(null, rawTransaction, signature); 1584 | String hexValue = "0x" + Hex.toHexString(signedMessage); 1585 | EthSendTransaction ethSendTransaction = web3j.ethSendRawTransaction(hexValue).send(); 1586 | 1587 | if (ethSendTransaction.hasError()) { 1588 | System.out.println("Transaction Error: " + ethSendTransaction.getError().getMessage()); 1589 | } 1590 | 1591 | assertFalse(ethSendTransaction.hasError()); 1592 | } 1593 | 1594 | @Test 1595 | @DisplayName("Performance Test") 1596 | @Tag("manual") 1597 | void performanceTest() throws Exception { 1598 | long time, deriveAccount = 0, deriveParent = 0, deriveParentHardened = 0; 1599 | final long SAMPLE_COUNT = 10; 1600 | 1601 | System.out.println("Measuring key derivation performance. All times are expressed in milliseconds"); 1602 | System.out.println("***********************************************" ); 1603 | 1604 | // Prepare the card 1605 | cmdSet.autoOpenSecureChannel(); 1606 | APDUResponse response = cmdSet.verifyPIN("000000"); 1607 | assertEquals(0x9000, response.getSw()); 1608 | KeyPairGenerator g = keypairGenerator(); 1609 | KeyPair keyPair = g.generateKeyPair(); 1610 | byte[] chainCode = new byte[32]; 1611 | new Random().nextBytes(chainCode); 1612 | 1613 | response = cmdSet.loadKey(keyPair, false, chainCode); 1614 | assertEquals(0x9000, response.getSw()); 1615 | 1616 | for (int i = 0; i < SAMPLE_COUNT; i++) { 1617 | time = System.currentTimeMillis(); 1618 | response = cmdSet.deriveKey(new byte[] { (byte) 0x80, 0x00, 0x00, 0x2C, (byte) 0x80, 0x00, 0x00, 0x3C, (byte) 0x80, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00, (byte) 0x00, 0x00, 0x00, 0x00}, KeycardApplet.DERIVE_P1_SOURCE_MASTER); 1619 | deriveAccount += System.currentTimeMillis() - time; 1620 | assertEquals(0x9000, response.getSw()); 1621 | } 1622 | 1623 | deriveAccount /= SAMPLE_COUNT; 1624 | 1625 | for (int i = 0; i < SAMPLE_COUNT; i++) { 1626 | time = System.currentTimeMillis(); 1627 | response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, (byte) i}, KeycardApplet.DERIVE_P1_SOURCE_PARENT); 1628 | deriveParent += System.currentTimeMillis() - time; 1629 | assertEquals(0x9000, response.getSw()); 1630 | } 1631 | 1632 | deriveParent /= SAMPLE_COUNT; 1633 | 1634 | for (int i = 0; i < SAMPLE_COUNT; i++) { 1635 | time = System.currentTimeMillis(); 1636 | response = cmdSet.deriveKey(new byte[] {(byte) 0x80, 0x00, 0x00, (byte) i}, KeycardApplet.DERIVE_P1_SOURCE_PARENT); 1637 | deriveParentHardened += System.currentTimeMillis() - time; 1638 | assertEquals(0x9000, response.getSw()); 1639 | } 1640 | 1641 | deriveParentHardened /= SAMPLE_COUNT; 1642 | 1643 | System.out.println("Time to derive m/44'/60'/0'/0/0: " + deriveAccount); 1644 | System.out.println("Time to switch m/44'/60'/0'/0/0': " + deriveParentHardened); 1645 | System.out.println("Time to switch back to m/44'/60'/0'/0/0: " + deriveParent); 1646 | } 1647 | 1648 | private KeyPairGenerator keypairGenerator() throws Exception { 1649 | ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1"); 1650 | KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH", "BC"); 1651 | g.initialize(ecSpec); 1652 | 1653 | return g; 1654 | } 1655 | 1656 | private byte[] extractSignature(byte[] sig) { 1657 | int off = sig[4] + 5; 1658 | return Arrays.copyOfRange(sig, off, off + sig[off + 1] + 2); 1659 | } 1660 | 1661 | private byte[] extractPublicKeyFromSignature(byte[] sig) { 1662 | assertEquals(KeycardApplet.TLV_SIGNATURE_TEMPLATE, sig[0]); 1663 | assertEquals((byte) 0x81, sig[1]); 1664 | assertEquals(KeycardApplet.TLV_PUB_KEY, sig[3]); 1665 | 1666 | return Arrays.copyOfRange(sig, 5, 5 + sig[4]); 1667 | } 1668 | 1669 | private void reset() { 1670 | switch(TARGET) { 1671 | case TARGET_SIMULATOR: 1672 | simulator.reset(); 1673 | break; 1674 | case TARGET_CARD: 1675 | apduChannel.getCard().getATR(); 1676 | break; 1677 | default: 1678 | break; 1679 | } 1680 | } 1681 | 1682 | private void resetAndSelectAndOpenSC() throws Exception { 1683 | if (cmdSet.getApplicationInfo().hasSecureChannelCapability()) { 1684 | reset(); 1685 | cmdSet.select(); 1686 | cmdSet.autoOpenSecureChannel(); 1687 | } 1688 | } 1689 | 1690 | private void assertMnemonic(int expectedLength, byte[] data) { 1691 | short[] shorts = new short[data.length / 2]; 1692 | assertEquals(expectedLength, shorts.length); 1693 | ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN).asShortBuffer().get(shorts); 1694 | 1695 | boolean[] bits = new boolean[11 * shorts.length]; 1696 | int i = 0; 1697 | 1698 | for (short mIdx : shorts) { 1699 | assertTrue(mIdx >= 0 && mIdx < 2048); 1700 | for (int j = 0; j < 11; ++j) { 1701 | bits[i++] = (mIdx & (1 << (10 - j))) > 0; 1702 | } 1703 | } 1704 | 1705 | data = new byte[bits.length / 33 * 4]; 1706 | 1707 | for (i = 0; i < bits.length / 33 * 32; ++i) { 1708 | data[i / 8] |= (bits[i] ? 1 : 0) << (7 - (i % 8)); 1709 | } 1710 | 1711 | byte[] check = sha256(data); 1712 | 1713 | for (i = bits.length / 33 * 32; i < bits.length; ++i) { 1714 | if ((check[(i - bits.length / 33 * 32) / 8] & (1 << (7 - (i % 8))) ^ (bits[i] ? 1 : 0) << (7 - (i % 8))) != 0) { 1715 | fail("Checksum is invalid"); 1716 | } 1717 | } 1718 | } 1719 | 1720 | private void verifyKeyDerivation(KeyPair keyPair, byte[] chainCode, int[] path) throws Exception { 1721 | byte[] hash = sha256(new byte[8]); 1722 | APDUResponse resp = cmdSet.sign(hash); 1723 | assertEquals(0x9000, resp.getSw()); 1724 | byte[] sig = resp.getData(); 1725 | byte[] publicKey = extractPublicKeyFromSignature(sig); 1726 | sig = extractSignature(sig); 1727 | 1728 | if (cmdSet.getApplicationInfo().hasKeyManagementCapability()) { 1729 | DeterministicKey key = deriveKey(keyPair, chainCode, path); 1730 | 1731 | assertTrue(key.verify(hash, sig)); 1732 | assertArrayEquals(key.getPubKeyPoint().getEncoded(false), publicKey); 1733 | } else { 1734 | Signature signature = Signature.getInstance("SHA256withECDSA", "BC"); 1735 | 1736 | ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1"); 1737 | ECPublicKeySpec cardKeySpec = new ECPublicKeySpec(ecSpec.getCurve().decodePoint(publicKey), ecSpec); 1738 | ECPublicKey cardKey = (ECPublicKey) KeyFactory.getInstance("ECDSA", "BC").generatePublic(cardKeySpec); 1739 | 1740 | signature.initVerify(cardKey); 1741 | signature.update(new byte[8]); 1742 | assertTrue(signature.verify(sig)); 1743 | } 1744 | 1745 | resp = cmdSet.getStatus(KeycardApplet.GET_STATUS_P1_KEY_PATH); 1746 | assertEquals(0x9000, resp.getSw()); 1747 | byte[] rawPath = resp.getData(); 1748 | 1749 | assertEquals(path.length * 4, rawPath.length); 1750 | 1751 | for (int i = 0; i < path.length; i++) { 1752 | int k = path[i]; 1753 | int k1 = (rawPath[i * 4] << 24) | (rawPath[(i * 4) + 1] << 16) | (rawPath[(i * 4) + 2] << 8) | rawPath[(i * 4) + 3]; 1754 | assertEquals(k, k1); 1755 | } 1756 | } 1757 | 1758 | private void verifyExportedKey(byte[] keyTemplate, KeyPair keyPair, byte[] chainCode, int[] path, boolean publicOnly, boolean extendedPublic) { 1759 | if (!cmdSet.getApplicationInfo().hasKeyManagementCapability()) { 1760 | return; 1761 | } 1762 | 1763 | DeterministicKey dk = deriveKey(keyPair, chainCode, path); 1764 | ECKey key = dk.decompress(); 1765 | assertEquals(KeycardApplet.TLV_KEY_TEMPLATE, keyTemplate[0]); 1766 | 1767 | if (publicOnly) { 1768 | assertEquals(KeycardApplet.TLV_PUB_KEY, keyTemplate[2]); 1769 | byte[] pubKey = Arrays.copyOfRange(keyTemplate, 4, 4 + keyTemplate[3]); 1770 | 1771 | assertArrayEquals(key.getPubKey(), pubKey); 1772 | int templateLen = 2 + pubKey.length; 1773 | 1774 | if (extendedPublic) { 1775 | byte[] chain = Arrays.copyOfRange(keyTemplate, templateLen + 4, templateLen + 4 + keyTemplate[3 + templateLen]); 1776 | assertEquals(KeycardApplet.TLV_CHAIN_CODE, keyTemplate[2 + templateLen]); 1777 | assertArrayEquals(dk.getChainCode(), chain); 1778 | templateLen += 2 + chain.length; 1779 | } 1780 | 1781 | assertEquals(templateLen, keyTemplate[1]); 1782 | assertEquals(templateLen + 2, keyTemplate.length); 1783 | } else { 1784 | assertEquals(KeycardApplet.TLV_PRIV_KEY, keyTemplate[2]); 1785 | byte[] privateKey = Arrays.copyOfRange(keyTemplate, 4, 4 + keyTemplate[3]); 1786 | 1787 | byte[] tPrivKey = key.getPrivKey().toByteArray(); 1788 | 1789 | if (tPrivKey[0] == 0x00) { 1790 | tPrivKey = Arrays.copyOfRange(tPrivKey, 1, tPrivKey.length); 1791 | } 1792 | 1793 | assertArrayEquals(tPrivKey, privateKey); 1794 | } 1795 | } 1796 | 1797 | private DeterministicKey deriveKey(KeyPair keyPair, byte[] chainCode, int[] path) { 1798 | DeterministicKey key = HDKeyDerivation.createMasterPrivKeyFromBytes(((org.bouncycastle.jce.interfaces.ECPrivateKey) keyPair.getPrivate()).getD().toByteArray(), chainCode); 1799 | 1800 | for (int i : path) { 1801 | key = HDKeyDerivation.deriveChildKey(key, new ChildNumber(i)); 1802 | } 1803 | 1804 | return key; 1805 | } 1806 | 1807 | private boolean isMalleable(byte[] sig) { 1808 | int rLen = sig[3]; 1809 | int sOff = 6 + rLen; 1810 | int sLen = sig.length - rLen - 6; 1811 | 1812 | BigInteger s = new BigInteger(Arrays.copyOfRange(sig, sOff, sOff + sLen)); 1813 | BigInteger limit = new BigInteger("7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0", 16); 1814 | 1815 | return s.compareTo(limit) >= 1; 1816 | } 1817 | 1818 | /** 1819 | * Signs a signature using the card. Returns a SignatureData object which contains v, r and s. The algorithm to do 1820 | * this is as follow: 1821 | * 1822 | * 1) The Keccak-256 hash of transaction is generated off-card 1823 | * 2) A SIGN command is sent to the card to sign the precomputed hash 1824 | * 3) The returned data is the public key and the signature 1825 | * 4) The signature and public key can be used to generate the v value. The v value allows to recover the public key 1826 | * from the signature. Here we use the web3j implementation through reflection 1827 | * 5) v, r and s are the final signature to append to the transaction 1828 | * 1829 | * @param message the raw transaction 1830 | * @return the signature data 1831 | */ 1832 | private Sign.SignatureData signMessage(byte[] message) throws Exception { 1833 | byte[] messageHash = Hash.sha3(message); 1834 | 1835 | APDUResponse response = cmdSet.sign(messageHash); 1836 | assertEquals(0x9000, response.getSw()); 1837 | byte[] respData = response.getData(); 1838 | byte[] rawSig = extractSignature(respData); 1839 | 1840 | int rLen = rawSig[3]; 1841 | int sOff = 6 + rLen; 1842 | int sLen = rawSig.length - rLen - 6; 1843 | 1844 | BigInteger r = new BigInteger(Arrays.copyOfRange(rawSig, 4, 4 + rLen)); 1845 | BigInteger s = new BigInteger(Arrays.copyOfRange(rawSig, sOff, sOff + sLen)); 1846 | 1847 | Class ecdsaSignature = Class.forName("org.web3j.crypto.Sign$ECDSASignature"); 1848 | Constructor ecdsaSignatureConstructor = ecdsaSignature.getDeclaredConstructor(BigInteger.class, BigInteger.class); 1849 | ecdsaSignatureConstructor.setAccessible(true); 1850 | Object sig = ecdsaSignatureConstructor.newInstance(r, s); 1851 | Method m = ecdsaSignature.getMethod("toCanonicalised"); 1852 | m.setAccessible(true); 1853 | sig = m.invoke(sig); 1854 | 1855 | Method recoverFromSignature = Sign.class.getDeclaredMethod("recoverFromSignature", int.class, ecdsaSignature, byte[].class); 1856 | recoverFromSignature.setAccessible(true); 1857 | 1858 | byte[] pubData = extractPublicKeyFromSignature(respData); 1859 | BigInteger publicKey = new BigInteger(Arrays.copyOfRange(pubData, 1, pubData.length)); 1860 | 1861 | int recId = -1; 1862 | for (int i = 0; i < 4; i++) { 1863 | BigInteger k = (BigInteger) recoverFromSignature.invoke(null, i, sig, messageHash); 1864 | if (k != null && k.equals(publicKey)) { 1865 | recId = i; 1866 | break; 1867 | } 1868 | } 1869 | if (recId == -1) { 1870 | throw new RuntimeException("Could not construct a recoverable key. This should never happen."); 1871 | } 1872 | 1873 | int headerByte = recId + 27; 1874 | 1875 | Field rF = ecdsaSignature.getDeclaredField("r"); 1876 | rF.setAccessible(true); 1877 | Field sF = ecdsaSignature.getDeclaredField("s"); 1878 | sF.setAccessible(true); 1879 | r = (BigInteger) rF.get(sig); 1880 | s = (BigInteger) sF.get(sig); 1881 | 1882 | // 1 header + 32 bytes for R + 32 bytes for S 1883 | byte v = (byte) headerByte; 1884 | byte[] rB = Numeric.toBytesPadded(r, 32); 1885 | byte[] sB = Numeric.toBytesPadded(s, 32); 1886 | 1887 | return new Sign.SignatureData(v, rB, sB); 1888 | } 1889 | 1890 | private void verifyKeyUID(byte[] keyUID, ECPublicKey pubKey) { 1891 | verifyKeyUID(keyUID, pubKey.getQ().getEncoded(false)); 1892 | } 1893 | 1894 | private void verifyKeyUID(byte[] keyUID, byte[] pubKey) { 1895 | assertArrayEquals(sha256(pubKey), keyUID); 1896 | } 1897 | } 1898 | --------------------------------------------------------------------------------