diff --git a/.circleci/config.yml b/.circleci/config.yml index da82cb3..ef3291d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2 jobs: build: docker: - - image: circleci/openjdk:8u232-jdk + - image: mingc/android-build-box steps: - checkout - run: git submodule update --init diff --git a/.gitignore b/.gitignore index a9dff75..047d218 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ build # gradle stuff /.gradle +*.swp +libs/globalplatform-2_1_1/META-INF/ diff --git a/.gitmodules b/.gitmodules index e50db33..18f6774 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "datastorage"] path = datastorage url = ssh://git@github.com/idpass/card-storage-applet.git +[submodule "sign"] + path = sign + url = ssh://git@github.com/idpass/card-sign-applet.git diff --git a/auth b/auth index 301ecdf..d9b3665 160000 --- a/auth +++ b/auth @@ -1 +1 @@ -Subproject commit 301ecdf025c42da40cf9c780d80684c5efa74855 +Subproject commit d9b3665340615ac1a305851a01d4ffa26e11456e diff --git a/build.gradle b/build.gradle index aa9def4..aee0418 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,59 @@ +def getGitHash = { p -> + def stdout = new ByteArrayOutputStream() + exec { + if (p) workingDir p + commandLine 'git','rev-parse','HEAD' + standardOutput = stdout + } + return stdout.toString().trim() +} + +def getUri = { p -> + def stdout = new ByteArrayOutputStream() + exec { + if (p) workingDir p + commandLine 'git','remote','-v' + standardOutput = stdout + } + def repoUri = stdout.toString().trim().split("\n")[0].split()[1] + return repoUri +} + +def getHostname = { + def stdout = new ByteArrayOutputStream() + exec { + commandLine 'hostname' + standardOutput = stdout + } + def x = stdout.toString().trim() + return x +} + subprojects { final def rootPath = rootDir.absolutePath final def libs = rootPath + '/libs' final def libs_gp211 = rootPath + '/libs/globalplatform-2_1_1' + final def libsSdk = rootPath + '/libs-sdks' + final def libs_classes = rootPath + '/build/javacard/tools.exp' + + final def JC211 = libsSdk + '/jc211_kit' + final def JC212 = libsSdk + '/jc212_kit' + final def JC221 = libsSdk + '/jc221_kit' + final def JC222 = libsSdk + '/jc222_kit' + final def JC303 = libsSdk + '/jc303_kit' + final def JC304 = libsSdk + '/jc304_kit' + final def JC305u1 = libsSdk + '/jc305u1_kit' + final def JC305u2 = libsSdk + '/jc305u2_kit' + final def JC305u3 = libsSdk + '/jc305u3_kit' + + ext { + _getUri = getUri + _getGitHash = getGitHash + _getHostname = getHostname + _sourceCompatibility = 1.7 + _targetCompatibility = 1.7 + _JC_SELECTED = JC304 + } buildscript { repositories { @@ -11,7 +63,7 @@ subprojects { } dependencies { - classpath 'com.fidesmo:gradle-javacard:0.2.7' + classpath 'com.klinec:gradle-javacard:1.6.3' } } @@ -23,14 +75,12 @@ subprojects { flatDir { dirs libs dirs libs_gp211 + dirs libs_classes } } - - task wrapper(type: Wrapper) { - gradleVersion = '4.7' - } } allprojects { buildDir = new File(rootProject.projectDir, "build") } + diff --git a/build.sh b/build.sh index 3c283fe..83b9eb7 100755 --- a/build.sh +++ b/build.sh @@ -5,10 +5,14 @@ export JC_HOME=$(pwd)/libs-sdks/jc304_kit/ export _JAVA_OPTIONS=-Djc.home=$JC_HOME buildoutputs=' -build/javacard/org/idpass/auth/javacard/auth.cap -build/javacard/org/idpass/tools/javacard/tools.cap -build/javacard/org/idpass/sam/javacard/sam.cap -build/javacard/org/idpass/datastorage/javacard/datastorage.cap' +build/javacard/auth.cap +build/javacard/tools.exp/org/idpass/tools/javacard/tools.cap +build/javacard/sam.cap +build/javacard/datastorage.cap +build/libs/idpass_tools.jar +build/libs/idpass_auth.jar +build/libs/idpass_datastorage.jar +build/libs/idpass_sam.jar' buildoutputscount=$(echo $buildoutputs | tr ' ' '\n' | wc -l) ./gradlew build diff --git a/datastorage b/datastorage index 80a019c..5dc7f9c 160000 --- a/datastorage +++ b/datastorage @@ -1 +1 @@ -Subproject commit 80a019c93b014efdab4ba4ab892bf044029b64c6 +Subproject commit 5dc7f9cfa488240927b4e7d282a275e8cac71ced diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 073ef12..cca4638 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ #Mon Dec 02 10:05:57 SGT 2019 -distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists diff --git a/offcard/GPC_CardSpecification_v2.3.1_PublicRelease_CC.pdf b/offcard/GPC_CardSpecification_v2.3.1_PublicRelease_CC.pdf new file mode 100755 index 0000000..b3b5cd7 Binary files /dev/null and b/offcard/GPC_CardSpecification_v2.3.1_PublicRelease_CC.pdf differ diff --git a/offcard/README.md b/offcard/README.md new file mode 100644 index 0000000..1fb8c1a --- /dev/null +++ b/offcard/README.md @@ -0,0 +1,39 @@ +# GlobalPlatform Card Spec 2.3.1 + +As latest specification contains errata and precision of previous versions of the spec, +therefore will use latest specification url https://globalplatform.org/specs-library/card-specification-v2-3-1/ + +Older specification, for example, cited a dubious usage advise of `p2` in the `INITIALIZE_UPDATE` command. +The latest specification `v2.3.1` explicitly clarifies the value of `p2` to be always `0x00`. A mis-interpretation +of this tiny detail would lead you to think of `p2` as an option to choose one of the key. + +### Applet privileges +- b8=1 indicates that the Application is a Security Domain. +- b7=1 indicates that the Security Domain has DAP Verification capability. +- b6=1 indicates that the Security Domain has Delegated Management privileges. +- b5=1 indicates that the Application has the privilege to lock the card. +- b4=1 indicates that the Application has the privilege to terminate the card. +- b3=1 indicates that the Application has the Default Selected privilege. +- b2=1 indicates that the Application has CVM management privileges. +- b1=1 indicates that the Security Domain has mandated DAP Verification capability. + +### 11.1.4 Class Byte Coding (Card Specification v2.3.1) +- 0x00 Command defined in ISO/IEC 7816 +- 0x80 Proprietary command +- 0x84 Proprietary command with secure messaging + +### Key Type +- 0x00 - 0x7F Reserved +- 0x80 DES - mode (EBC/CBC) implicitely known +- ... + +### Miscelaneous (from specs) +- The `ISD` shall be the Default Selected Application +- An initial key shall be available within the `ISD` + +### Miscelaneous (from observation) +- Once a key is added, the default factory `kvno` of `0xFF` with default key `40 .. 4F` is forever lost. The offcard must explicitely declare a keyset. +- One a key is added, it cannot be deleted. But only replaced with new key value +- In the JCOP terminal, `/send` != `send`. These are the insecure and secure variations of sending an apdu +- Once a data attempts to go out from an applet **insecurely**, it resets the applet's security level to 0x00. The JCOP terminal still thinks 0x83 though. +- Always first load `tools.cap` diff --git a/offcard/build.gradle b/offcard/build.gradle new file mode 100644 index 0000000..c31e651 --- /dev/null +++ b/offcard/build.gradle @@ -0,0 +1,33 @@ +group 'org.idpass.offcard' +version '0.0.1' + +apply plugin: 'java' +sourceCompatibility = 1.8 + +dependencies { + compile 'com.klinec:jcardsim:3.0.5.9' + compile 'org.testng:testng:7.0.0' + implementation 'org.bouncycastle:bcprov-jdk15on:1.62' + implementation 'org.web3j:core:2.3.1' + implementation 'org.bitcoinj:bitcoinj-core:0.14.5' + + // Establish build order dependency of + // org.idpass.offcard.applet/* to org.idpass.{auth,sam,datastorage}/* + compile project(':auth') + compile project(':sam') + compile project(':datastorage') + compile project(':sign') +} + +test { + println "--- offcard test task ---" + filter { + includeTestsMatching "org.idpass.offcard.test.Main.*" + } + + testLogging.showStandardStreams = true + outputs.upToDateWhen {false} + useTestNG() + jvmArgs '-noverify' +} + diff --git a/offcard/src/main/java/org/idpass/dev/DecodeApplet.java b/offcard/src/main/java/org/idpass/dev/DecodeApplet.java new file mode 100644 index 0000000..8ece92a --- /dev/null +++ b/offcard/src/main/java/org/idpass/dev/DecodeApplet.java @@ -0,0 +1,355 @@ +package org.idpass.dev; + +import org.globalplatform.GPSystem; +import org.globalplatform.SecureChannel; + +import javacard.framework.APDU; +import javacard.framework.APDUException; +import javacard.framework.Applet; +import javacard.framework.AppletEvent; +import javacard.framework.ISO7816; +import javacard.framework.ISOException; +import javacard.framework.JCSystem; +import javacard.framework.Util; +import javacardx.apdu.ExtendedLength; + +/* +This is a stand-alone small scale replica of IdpassApplet. It will be used to +diagnose unusual issues or to probe a card's internal state: + - memory + - security level +*/ +public class DecodeApplet extends Applet implements ExtendedLength, AppletEvent +{ + private static final byte INS_NOOP = (byte)0x00; + private static final byte INS_ECHO = (byte)0x01; + private static final byte INS_CONTROL = (byte)0x02; + + public final static class Utils + { + public static final byte BYTE_00 = (byte)0x00; + public static final short SHORT_00 = (short)0x0000; + + // Call JCSystem.requestObjectDeletion if Supported + public static void requestObjectDeletion() + { + if (JCSystem.isObjectDeletionSupported()) { + JCSystem.requestObjectDeletion(); + } + } + + private Utils() + { + } + } + + public final static short LENGTH_APDU_EXTENDED = (short)0x7FFF; + private static final byte INS_INITIALIZE_UPDATE = (byte)0x50; + private static final byte INS_BEGIN_RMAC_SESSION = (byte)0x7A; + private static final byte INS_END_RMAC_SESSION = (byte)0x78; + + protected static final byte MASK_GP = (byte)0x80; + protected static final byte MASK_SECURED = (byte)0x0C; + + private byte[] apduData; + protected byte cla; + protected byte ins; + protected byte p1; + protected byte p2; + + protected SecureChannel secureChannel; + + private byte control; + private byte[] m_memo; + + public static void install(byte[] bArray, short bOffset, byte bLength) + { + DecodeApplet applet = new DecodeApplet(bArray, bOffset, bLength); + + // GP-compliant JavaCard applet registration + applet.register(bArray, (short)(bOffset + 1), bArray[bOffset]); + } + + protected DecodeApplet(byte[] bArray, short bOffset, byte bLength) + { + byte lengthAID = bArray[bOffset]; + short offsetAID = (short)(bOffset + 1); + short offset = bOffset; + offset += (bArray[offset]); // skip aid + offset++; + offset += (bArray[offset]); // skip privileges + offset++; + + // read params + short lengthIn = bArray[offset]; + if (lengthIn != 0) { + this.control = bArray[(short)(offset + 1)]; + } + } + + @Override public void uninstall() + { + apduData = null; + } + + @Override public void process(APDU apdu) throws ISOException + { + try { + byte[] buffer = apdu.getBuffer(); + cla = buffer[ISO7816.OFFSET_CLA]; + ins = buffer[ISO7816.OFFSET_INS]; + p1 = buffer[ISO7816.OFFSET_P1]; + p2 = buffer[ISO7816.OFFSET_P2]; + + // ISO class + if ((cla & (~MASK_SECURED)) == ISO7816.CLA_ISO7816) { + if (ins == ISO7816.INS_SELECT) { + processSelect(); + return; + } + } + + switch (ins) { + case INS_INITIALIZE_UPDATE: + case ISO7816.INS_EXTERNAL_AUTHENTICATE: + case INS_BEGIN_RMAC_SESSION: + case INS_END_RMAC_SESSION: + checkClaIsGp(); + // allow to make contactless SCP + // checkProtocolContacted(); + processSecurity(); + break; + default: + processInternal(apdu); + } + + } finally { + if (apduData != null) { + apduData = null; + Utils.requestObjectDeletion(); + } + } + } + //////////////////////////////////////////////////////////////////////////// + public boolean select() + { + secureChannel = GPSystem.getSecureChannel(); + return true; + } + + public void deselect() + { + // free the handle of the Security Domain associated with this applet. + secureChannel.resetSecurity(); + } + + void processSelect() + { + if (!selectingApplet()) { + ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED); + } + + setIncomingAndReceiveUnwrap(); + + byte[] buffer = getApduData(); + + // short length = Util.setShort(buffer, Utils.SHORT_00, + // personasRepository.getPersonasCount()); + // setOutgoingAndSendWrap(buffer, Utils.SHORT_00, length); + } + + protected void processSecurity() + { + // send to ISD + short responseLength + = secureChannel.processSecurity(APDU.getCurrentAPDU()); + if (responseLength != 0) { + APDU.getCurrentAPDU().setOutgoingAndSend( + (short)ISO7816.OFFSET_CDATA, responseLength); + } + } + + protected void checkClaIsGp() + { + if ((cla & MASK_GP) != MASK_GP) { + ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED); + } + } + + protected void processInternal(APDU apdu) throws ISOException + { + switch (this.ins) { + case INS_NOOP: + ins_noop(apdu); + break; + case INS_ECHO: + ins_echo(apdu); + break; + case INS_CONTROL: + ins_control(apdu); + break; + default: + ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); + } + } + //////////////////////////////////////////////////////////////////////////// + protected byte[] getApduData() + { + if (APDU.getCurrentAPDU().getCurrentState() + < APDU.STATE_PARTIAL_INCOMING) { + APDUException.throwIt(APDUException.ILLEGAL_USE); + } + if (apduData == null) { + return APDU.getCurrentAPDUBuffer(); + } else { + return apduData; + } + } + + protected short setIncomingAndReceiveUnwrap() + { + byte[] buffer = APDU.getCurrentAPDUBuffer(); + short bytesRead = APDU.getCurrentAPDU().setIncomingAndReceive(); + short apduDataOffset = APDU.getCurrentAPDU().getOffsetCdata(); + boolean isExtendedLengthData + = apduDataOffset == ISO7816.OFFSET_EXT_CDATA; + short overallLength = APDU.getCurrentAPDU().getIncomingLength(); + + if (isExtendedLengthData) { + apduData = new byte[LENGTH_APDU_EXTENDED]; + + Util.arrayCopyNonAtomic(buffer, + (short)0, + apduData, + (short)0, + (short)(apduDataOffset + bytesRead)); + + if (bytesRead != overallLength) { // otherwise we're finished, all + // bytes received + short received = 0; + do { + received = APDU.getCurrentAPDU().receiveBytes((short)0); + Util.arrayCopyNonAtomic(buffer, + (short)0, + apduData, + (short)(apduDataOffset + bytesRead), + received); + bytesRead += received; + } while (!(received == 0 || bytesRead == overallLength)); + } + + buffer = apduData; + } + + short result = overallLength; + + byte sl = secureChannel.getSecurityLevel(); + if ((sl & SecureChannel.C_DECRYPTION) != 0 + || (sl & SecureChannel.C_MAC) != 0) { + result = (short)(secureChannel.unwrap( + buffer, + (short)0, + (short)(apduDataOffset + overallLength)) + - apduDataOffset); + } + + Util.arrayCopyNonAtomic( + buffer, apduDataOffset, buffer, (short)0, result); + + short bytesLeft = (short)(apduDataOffset - result); + if (bytesLeft > 0) { + Util.arrayFillNonAtomic(buffer, + (short)(apduDataOffset - bytesLeft), + bytesLeft, + (byte)0); + } + return result; + } + + protected void setOutgoingAndSendWrap(byte[] buffer, short bOff, short len) + { + if (APDU.getCurrentAPDU().getCurrentState() < APDU.STATE_OUTGOING) { + APDU.getCurrentAPDU().setOutgoing(); + } + + byte sl = secureChannel.getSecurityLevel(); + + if ((sl & SecureChannel.R_ENCRYPTION) != 0 + || (sl & SecureChannel.R_MAC) != 0) { + len = secureChannel.wrap(buffer, bOff, len); + } + + APDU.getCurrentAPDU().setOutgoingLength(len); + APDU.getCurrentAPDU().sendBytesLong(buffer, bOff, len); + } + //////////////////////////////////////////////////////////////////////////// + protected SecureChannel getSecurityObject() + { + return secureChannel; + } + + protected boolean isCheckC_MAC() + { + byte sl = secureChannel.getSecurityLevel(); + + if ((cla & MASK_SECURED) > 0) { + if (((sl & SecureChannel.AUTHENTICATED) == 0) + || ((sl & SecureChannel.C_MAC) == 0)) { + ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + return true; + } else { + if ((sl & SecureChannel.AUTHENTICATED) != 0) { + ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + return false; + } + } + + protected boolean isCheckC_DECRYPTION() + { + byte sl = secureChannel.getSecurityLevel(); + + if ((cla & MASK_SECURED) > 0) { + if (((sl & SecureChannel.AUTHENTICATED) == 0) + || ((sl & SecureChannel.C_DECRYPTION) == 0)) { + ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + return true; + } else { + if ((sl & SecureChannel.AUTHENTICATED) != 0) { + ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + return false; + } + } + //////////////////////////////////////////////////////////////////////////// + public void ins_noop(APDU apdu) + { + } + + public void ins_echo(APDU apdu) + { + if ((control & 0x01) != 0) { + if (!(isCheckC_MAC())) { + ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + } + + if ((control & 0x02) != 0) { + if (!(isCheckC_DECRYPTION())) { + ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + } + + short lc = setIncomingAndReceiveUnwrap(); + byte[] buffer = getApduData(); + + setOutgoingAndSendWrap(buffer, Utils.SHORT_00, lc); + } + + public void ins_control(APDU apdu) + { + control = p1; + } +} diff --git a/offcard/src/main/java/org/idpass/dev/build.gradle b/offcard/src/main/java/org/idpass/dev/build.gradle new file mode 100644 index 0000000..759e9f3 --- /dev/null +++ b/offcard/src/main/java/org/idpass/dev/build.gradle @@ -0,0 +1,54 @@ +group 'card-applets' +apply plugin: 'javacard' + +compileJava { + dependsOn 'buildJavaCard' +} + +buildJavaCard { + println "Building tracer applet..." +} + +jar { + manifest { + attributes ("Uri":_getGitHash(projectDir)) + attributes ("Commit-hash":_getUri(projectDir)) + } + baseName 'idpass_decode' +} + +javacard { + config { + jckit _JC_SELECTED + // Using custom repo with jcardsim + addSurrogateJcardSimRepo false + addImplicitJcardSim false + addImplicitJcardSimJunit false + + cap { + sourceSets { + main.java.srcDirs = ['.'] + } + + packageName = 'org.idpass.dev' + version = '0.1' + aid = '0xDE:0xC0:0xDE:0x00:0x00' + output 'decode.cap' + + applet { + className = 'DecodeApplet' + aid = '0xDE:0xC0:0xDE:0x00:0x00:0x01' + } + + dependencies { + remote 'local:gp211:2.1.1' + } + } + } +} + +compileJava { + sourceCompatibility = _sourceCompatibility + targetCompatibility = _targetCompatibility +} + diff --git a/offcard/src/main/java/org/idpass/offcard/applet/AuthApplet.java b/offcard/src/main/java/org/idpass/offcard/applet/AuthApplet.java new file mode 100644 index 0000000..064bc49 --- /dev/null +++ b/offcard/src/main/java/org/idpass/offcard/applet/AuthApplet.java @@ -0,0 +1,243 @@ +package org.idpass.offcard.applet; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import javax.smartcardio.CommandAPDU; +import javax.smartcardio.ResponseAPDU; + +import org.idpass.offcard.misc.Helper.Mode; +import org.idpass.offcard.misc.IdpassConfig; +import org.idpass.offcard.misc.Invariant; +import org.idpass.offcard.misc.Dump; + +import com.licel.jcardsim.bouncycastle.util.encoders.Hex; + +import javacard.framework.SystemException; + +import org.idpass.offcard.proto.OffCard; + +@IdpassConfig( + packageAID = "F769647061737301", + appletAID = "F769647061737301010001", + instanceAID = "F76964706173730101000101", + capFile = "auth.cap", + installParams = { + (byte)0x00, // PIN = 0x00, FINGERPRINT = 0x03 + (byte)0x01, + (byte)0x9E, + }, + privileges = { + (byte)0xFF, + (byte)0xFF, + } +) +public class AuthApplet extends org.idpass.auth.AuthApplet +{ + private static byte[] id_bytes; + private static Invariant Assert = new Invariant(); + private static AuthApplet instance; + + public static AuthApplet getInstance() + { + return instance; + } + + public static void install(byte[] bArray, short bOffset, byte bLength) + { + AuthApplet applet = new AuthApplet(bArray, bOffset, bLength); + + try { + applet.register(bArray, (short)(bOffset + 1), bArray[bOffset]); + } catch (SystemException e) { + Assert.assertTrue(OffCard.getInstance().getMode() != Mode.SIM, + "AuthApplet::install"); + } + instance = applet; + } + + @Override public final boolean select() + { + if (secureChannel == null) { + secureChannel = DummyISDApplet.getInstance().getSecureChannel(); + } + secureChannel.resetSecurity(); + return true; + } + + public byte[] SELECT() + { + return OffCard.getInstance().select(AuthApplet.class); + } + + private AuthApplet(byte[] bArray, short bOffset, byte bLength) + { + super(bArray, bOffset, bLength); + } + + public byte[] aid() + { + if (id_bytes == null) { + IdpassConfig cfg + = AuthApplet.class.getAnnotation(IdpassConfig.class); + String strId = cfg.instanceAID(); + id_bytes = Hex.decode(strId); + } + + return id_bytes; + } + //////////////////////////////////////////////////////////////////////////// + // processAddPersona + public short processAddPersona() + { + short newPersonaIndex = (short)0xFFFF; + CommandAPDU command = new CommandAPDU(0x00, 0x1A, 0x00, 0x00); + ResponseAPDU response; + + response = OffCard.getInstance().Transmit(command); + if (0x9000 == response.getSW()) { + newPersonaIndex = ByteBuffer.wrap(response.getData()) + .order(ByteOrder.BIG_ENDIAN) + .getShort(); + } + + return newPersonaIndex; + } + + // processDeletePersona + public void processDeletePersona(byte personaIndex) + { + byte p2 = personaIndex; + CommandAPDU command = new CommandAPDU(0x00, 0x1D, 0x00, p2); + ResponseAPDU response; + response = OffCard.getInstance().Transmit(command); + if (response.getSW() == 0x9000) { + System.out.println("DP success"); + } + } + + // processAddListener + public short processAddListener(byte[] listener) + { + short newListenerIndex = (short)0xFFFF; + byte[] data = listener; + CommandAPDU command = new CommandAPDU(0x00, 0xAA, 0x00, 0x00, data); + ResponseAPDU response; + response = OffCard.getInstance().Transmit(command); + + if (0x9000 == response.getSW()) { + newListenerIndex = ByteBuffer.wrap(response.getData()) + .order(ByteOrder.BIG_ENDIAN) + .getShort(); + System.out.println( + String.format("AL retval = 0x%04X", newListenerIndex)); + } + return newListenerIndex; + } + + // processDeleteListener + public boolean processDeleteListener(byte[] listener) + { + byte[] status = null; + byte[] data = listener; + CommandAPDU command = new CommandAPDU(0x00, 0xDA, 0x00, 0x00, data); + ResponseAPDU response; + response = OffCard.getInstance().Transmit(command); + + if (0x9000 == response.getSW()) { + status = response.getData(); + Dump.print("DL retval", status); + } + return status != null && status[0] == 0x01; + } + + // processAddVerifierForPersona + public short processAddVerifierForPersona(byte personaId, byte[] authData) + { + short newVerifierIndex = (short)0xFFFF; + byte[] data = authData; + byte p2 = personaId; + CommandAPDU command = new CommandAPDU(0x00, 0x2A, 0x00, p2, data); + ResponseAPDU response; + response = OffCard.getInstance().Transmit(command); + + if (0x9000 == response.getSW()) { + newVerifierIndex = ByteBuffer.wrap(response.getData()) + .order(ByteOrder.BIG_ENDIAN) + .getShort(); + System.out.println( + String.format("AVP retval = 0x%04X", newVerifierIndex)); + } + return newVerifierIndex; + } + + // processDeleteVerifierFromPersona + public void processDeleteVerifierFromPersona(byte personaIndex, + byte verifierIndex) + { + byte p1 = personaIndex; + byte p2 = verifierIndex; + CommandAPDU command = new CommandAPDU(0x00, 0x2D, p1, p2); + ResponseAPDU response; + response = OffCard.getInstance().Transmit(command); + if (response.getSW() == 0x9000) { + System.out.println("DVP ok"); + } + } + + // processAuthenticatePersona + /* + cm> AUP ${candidate} + ##################################################### + AUTHENTICATE_PERSONA + candidate data: + 7F2E#(81#(268B8129A7402DAC91335793342B8437814237C24238D34238E0423EEE423F4F43433F44521A45662D956D664470745379F2527DE64286EF42905B8697939297A0919AF3929F8D94A2878FA3948FA4A250AB854CB0C651B8CF41B8DA51CAA050D03C4CD54D5DD7175BDBBB50E0255CE5415DE72C4CE7FE41F1B05EF2914EF9C880FC258B)) + ##################################################### + => 04 EF 1D CD 98 38 8A 48 44 DC FA 4B F3 9F E6 36 .....8.HD..K...6 + 40 D4 6B AD D7 4F 1A 8B 5D 7B 2E 3E AD 7D 92 15 @.k..O..]{.>.}.. + 34 4B C4 FA 63 08 38 77 7A 1F D4 9D 25 6D 7B 00 4K..c.8wz...%m{. + 35 7A 92 C7 3D 31 43 2A 10 2A 32 60 2A A2 A3 17 5z..=1C*.*2`*... + C2 08 22 7A D3 CF 9C E2 A9 DD 0E 29 CD 86 45 4E .."z.......)..EN + 79 5C E2 82 03 7F FC 7D 43 43 9F C2 02 69 1F C0 y\.....}CC...i.. + C7 0D 5A 75 76 27 75 62 72 41 65 36 34 DE 58 04 ..Zuv'ubrAe64.X. + E0 15 52 E3 48 03 84 FA 89 8F D7 F9 26 A0 2B CF ..R.H.......&.+. + 13 1D 98 AE 7C A7 86 1F 82 8B 21 8B 80 59 E8 C4 ....|.....!..Y.. + 81 92 F6 10 82 A6 C1 31 AF B8 9C D0 65 .......1....e + (186800 usec) + <= 00 00 40 00 90 00 ..@... + Status: No Error + + cm> /send "00 EF 1D CD #(${candidate})" + => 00 EF 1D CD 89 7F 2E 86 81 84 26 8B 81 29 A7 40 ..........&..).@ + 2D AC 91 33 57 93 34 2B 84 37 81 42 37 C2 42 38 -..3W.4+.7.B7.B8 + D3 42 38 E0 42 3E EE 42 3F 4F 43 43 3F 44 52 1A .B8.B>.B?OCC?DR. + 45 66 2D 95 6D 66 44 70 74 53 79 F2 52 7D E6 42 Ef-.mfDptSy.R}.B + 86 EF 42 90 5B 86 97 93 92 97 A0 91 9A F3 92 9F ..B.[........... + 8D 94 A2 87 8F A3 94 8F A4 A2 50 AB 85 4C B0 C6 ..........P..L.. + 51 B8 CF 41 B8 DA 51 CA A0 50 D0 3C 4C D5 4D 5D Q..A..Q..P. 0) { + command = new CommandAPDU(0x00, 0x00, 0x00, 0x00, data); + } else { + command = new CommandAPDU(0x00, 0x00, 0x00, 0x00); + } + + ResponseAPDU response; + response = OffCard.getInstance().Transmit(command); + + if (0x9000 == response.getSW()) { + } + } + + public byte[] ins_echo(byte[] input, int p1, int p2) + { + System.out.println(input.length); + byte[] data = {}; + CommandAPDU command + = new CommandAPDU(0x00, 0x01, (byte)p1, (byte)p2, input); + ResponseAPDU response; + + response = OffCard.getInstance().Transmit(command); + + if (0x9000 == response.getSW()) { + data = response.getData(); + if (data.length > 0) { + Dump.print(data, "ins_echo"); + } + } + + return data; + } + + public void ins_control(int p1) + { + CommandAPDU command = new CommandAPDU(0x00, 0x02, p1, 0x00); + ResponseAPDU response; + response = OffCard.getInstance().Transmit(command); + + if (0x9000 == response.getSW()) { + } + } +} diff --git a/offcard/src/main/java/org/idpass/offcard/applet/DummyISDApplet.java b/offcard/src/main/java/org/idpass/offcard/applet/DummyISDApplet.java new file mode 100644 index 0000000..926d660 --- /dev/null +++ b/offcard/src/main/java/org/idpass/offcard/applet/DummyISDApplet.java @@ -0,0 +1,174 @@ +package org.idpass.offcard.applet; + +import org.idpass.offcard.misc.Helper.Mode; +import org.idpass.offcard.misc.IdpassConfig; +import org.idpass.offcard.misc.Invariant; +import org.idpass.offcard.proto.OffCard; +import org.idpass.offcard.proto.SCP02Keys; +import org.idpass.offcard.proto.SCP02; + +import com.licel.jcardsim.bouncycastle.util.encoders.Hex; + +import javacard.framework.APDU; +import javacard.framework.Applet; +import javacard.framework.ISO7816; +import javacard.framework.ISOException; +import javacard.framework.SystemException; + +@IdpassConfig( + instanceAID = "A0000001510000", + installParams = { + (byte)0x00 + }, + privileges = { + (byte)0xFF, + (byte)0xFF, + } +) +public class DummyISDApplet extends Applet +{ + // clang-format off + // Keys inside the card + private static SCP02Keys cardKeys[] = { + new SCP02Keys("404142434445464748494a4b4c4d4e4F", // 1 + "404142434445464748494a4b4c4d4e4F", + "404142434445464748494a4b4c4d4e4F"), + + new SCP02Keys("DEC0DE0102030405060708090A0B0C0D", // 2 + "DEC0DE0102030405060708090A0B0C0D", + "DEC0DE0102030405060708090A0B0C0D"), + + new SCP02Keys("CAFEBABE0102030405060708090A0B0C", // 3 + "CAFEBABE0102030405060708090A0B0C", + "CAFEBABE0102030405060708090A0B0C"), + + new SCP02Keys("C0FFEE0102030405060708090A0B0C0D", // 4 + "C0FFEE0102030405060708090A0B0C0D", + "C0FFEE0102030405060708090A0B0C0D"), + }; + // clang-format on + + private static Invariant Assert = new Invariant(); + private static byte[] id_bytes; + private static DummyISDApplet instance; + + protected byte cla; + protected byte ins; + protected byte p1; + protected byte p2; + + public static DummyISDApplet getInstance() + { + return instance; + } + + public static void install(byte[] bArray, short bOffset, byte bLength) + { + DummyISDApplet applet = new DummyISDApplet(bArray, bOffset, bLength); + + try { + applet.register(bArray, (short)(bOffset + 1), bArray[bOffset]); + } catch (SystemException e) { + Assert.assertTrue(OffCard.getInstance().getMode() != Mode.SIM, + "DummyIssuerSecurityDomain::install"); + } + instance = applet; + } + + private SCP02 scp02; + + @Override public final boolean select() + { + if (scp02 == null) { + scp02 = new SCP02(cardKeys, "card"); + } + + return true; + } + + public byte[] SELECT() + { + return OffCard.getInstance().select(DummyISDApplet.class); + } + + public org.globalplatform.SecureChannel getSecureChannel() + { + return scp02; + } + + protected DummyISDApplet(byte[] bArray, short bOffset, byte bLength) + { + byte lengthAID = bArray[bOffset]; + short offsetAID = (short)(bOffset + 1); + short offset = bOffset; + offset += (bArray[offset]); // skip aid + offset++; + offset += (bArray[offset]); // skip privileges + offset++; + } + + public byte[] aid() + { + if (id_bytes == null) { + IdpassConfig cfg + = DummyISDApplet.class.getAnnotation(IdpassConfig.class); + String strId = cfg.instanceAID(); + id_bytes = Hex.decode(strId); + } + + return id_bytes; + } + + @Override public void process(APDU apdu) throws ISOException + { + Assert.assertTrue(scp02 != null, "Applet::secureChannel"); + Assert.assertTrue(scp02.userKeys.length > 0, "card keys"); + + byte[] buffer = apdu.getBuffer(); + + cla = buffer[ISO7816.OFFSET_CLA]; + ins = buffer[ISO7816.OFFSET_INS]; + p1 = buffer[ISO7816.OFFSET_P1]; + p2 = buffer[ISO7816.OFFSET_P2]; + + if ((cla & (SCP02.MASK_SECURED)) == ISO7816.CLA_ISO7816) { + if (ins == ISO7816.INS_SELECT) { + if (!selectingApplet()) { + ISOException.throwIt(ISO7816.SW_FUNC_NOT_SUPPORTED); + } + return; + } + } + + switch (ins) { + case SCP02.INS_INITIALIZE_UPDATE: + case ISO7816.INS_EXTERNAL_AUTHENTICATE: + case SCP02.INS_BEGIN_RMAC_SESSION: + case SCP02.INS_END_RMAC_SESSION: + checkClaIsGp(); + // allow to make contactless SCP + // checkProtocolContacted(); + processSecurity(); + break; + default: + ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED); + } + } + + protected void processSecurity() + { + // send to ISD + short responseLength = scp02.processSecurity(APDU.getCurrentAPDU()); + if (responseLength != 0) { + APDU.getCurrentAPDU().setOutgoingAndSend( + (short)ISO7816.OFFSET_CDATA, responseLength); + } + } + + protected void checkClaIsGp() + { + if ((cla & SCP02.MASK_GP) != SCP02.MASK_GP) { + ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED); + } + } +} diff --git a/offcard/src/main/java/org/idpass/offcard/applet/SamApplet.java b/offcard/src/main/java/org/idpass/offcard/applet/SamApplet.java new file mode 100644 index 0000000..7bea93f --- /dev/null +++ b/offcard/src/main/java/org/idpass/offcard/applet/SamApplet.java @@ -0,0 +1,133 @@ +package org.idpass.offcard.applet; + +import javax.smartcardio.CommandAPDU; +import javax.smartcardio.ResponseAPDU; + +import org.idpass.offcard.misc.Helper.Mode; +import org.idpass.offcard.misc.IdpassConfig; +import org.idpass.offcard.misc.Invariant; +import org.idpass.offcard.misc.Dump; + +import com.licel.jcardsim.bouncycastle.util.encoders.Hex; + +import javacard.framework.SystemException; + +import org.idpass.offcard.proto.OffCard; + +@IdpassConfig( + packageAID = "F769647061737302", + appletAID = "F769647061737302010001", + instanceAID = "F76964706173730201000101", + capFile = "sam.cap", + installParams = { + (byte)0x9E, + }, + privileges = { + (byte)0xFF, + (byte)0xFF, + } +) +public final class SamApplet extends org.idpass.sam.SamApplet +{ + private static byte[] id_bytes; + private static Invariant Assert = new Invariant(); + private static SamApplet instance; + + public static SamApplet getInstance() + { + return instance; + } + + public static void install(byte[] bArray, short bOffset, byte bLength) + { + SamApplet applet = new SamApplet(bArray, bOffset, bLength); + + try { + applet.register(bArray, (short)(bOffset + 1), bArray[bOffset]); + } catch (SystemException e) { + Assert.assertTrue(OffCard.getInstance().getMode() != Mode.SIM, + "SamApplet::install"); + } + instance = applet; + } + + @Override public final boolean select() + { + if (secureChannel == null) { + secureChannel = DummyISDApplet.getInstance().getSecureChannel(); + } + return true; + } + + public byte[] SELECT() + { + return OffCard.getInstance().select(SamApplet.class); + } + + private SamApplet(byte[] bArray, short bOffset, byte bLength) + { + super(bArray, bOffset, bLength); + } + + public byte[] aid() + { + if (id_bytes == null) { + IdpassConfig cfg + = SamApplet.class.getAnnotation(IdpassConfig.class); + String strId = cfg.instanceAID(); + id_bytes = Hex.decode(strId); + } + + return id_bytes; + } + + @Override public void onPersonaAdded(short personaIndex) + { + super.onPersonaAdded(personaIndex); + System.out.println("SamApplet::onPersonaAdded"); + } + + @Override public void onPersonaDeleted(short personaIndex) + { + super.onPersonaDeleted(personaIndex); + System.out.println("SamApplet::onPersonaDeleted"); + } + + @Override + public void onPersonaAuthenticated(short personaIndex, short score) + { + super.onPersonaAuthenticated(personaIndex, score); + System.out.println("SamApplet::onPersonaAuthenticated"); + } + /////////////////////////////////////////////////////////////////////////// + + public byte[] processEncrypt(byte[] inData) + { + byte[] encryptedSigned = null; + byte[] data = inData; + CommandAPDU command = new CommandAPDU(0x00, 0xEC, 0x00, 0x00, data); + ResponseAPDU response; + response = OffCard.getInstance().Transmit(command); + + if (0x9000 == response.getSW()) { + encryptedSigned = response.getData(); + Dump.print("Encrypted by SamApplet", encryptedSigned); + } + return encryptedSigned; + } + + public byte[] processDecrypt(byte[] outData) + { + byte[] decryptedData = null; + byte[] data = outData; + CommandAPDU command = new CommandAPDU(0x00, 0xDC, 0x00, 0x00, data); + ResponseAPDU response; + response = OffCard.getInstance().Transmit(command); + + if (0x9000 == response.getSW()) { + decryptedData = response.getData(); + Dump.print("Decrypted by SamApplet", decryptedData); + } + return decryptedData; + } +} diff --git a/offcard/src/main/java/org/idpass/offcard/applet/SignApplet.java b/offcard/src/main/java/org/idpass/offcard/applet/SignApplet.java new file mode 100644 index 0000000..f101293 --- /dev/null +++ b/offcard/src/main/java/org/idpass/offcard/applet/SignApplet.java @@ -0,0 +1,330 @@ +package org.idpass.offcard.applet; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.Security; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; + +import org.web3j.crypto.ECKeyPair; + +import javax.crypto.KeyAgreement; + +import org.bouncycastle.jce.interfaces.ECPublicKey; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.interfaces.ECPrivateKey; + +import javax.smartcardio.CommandAPDU; +import javax.smartcardio.ResponseAPDU; + +import org.idpass.offcard.misc.Helper.Mode; +import org.idpass.offcard.misc.IdpassConfig; +import org.idpass.offcard.misc.Invariant; +import org.idpass.offcard.misc.Helper; +import org.idpass.offcard.proto.DataElement; +import org.idpass.offcard.proto.OffCard; + +import javacard.framework.SystemException; +import javacard.framework.Util; + +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.jce.spec.ECPublicKeySpec; +import org.bouncycastle.util.encoders.Hex; + +@IdpassConfig( + packageAID = "F769647061737304", + appletAID = "F769647061737304010001", + instanceAID = "F76964706173730401000101", + capFile = "sign.cap", + installParams = { + (byte)0x9E, + }, + privileges = { + (byte)0xFF, + (byte)0xFF, + } +) +public class SignApplet + extends org.idpass.sign.SignApplet +{ + static + { + Security.addProvider(new BouncyCastleProvider()); + } + + private static byte[] id_bytes; + private static SignApplet instance; + + private static Invariant Assert = new Invariant(); + + byte[] appletPub; + + private SecureRandom random; + + private ECParameterSpec ecSpec; + private KeyPairGenerator kpg; + + private KeyAgreement ka; + private KeyPair kp; + javacard.security.KeyPair jckp; + + private ECPublicKey pubKey; + javacard.security.ECPublicKey jcpubKey; + private ECPrivateKey privKey; + private Signature signer; + private javacard.security.Signature jcSigner; + private static KeyFactory kf; + + private byte[] sharedSecret; + + CommandAPDU command; + ResponseAPDU response; + byte[] lastResult; + + public static SignApplet getInstance() + { + return instance; + } + + public static void install(byte[] bArray, short bOffset, byte bLength) + { + SignApplet applet = new SignApplet(bArray, bOffset, bLength); + + try { + applet.register(bArray, (short)(bOffset + 1), bArray[bOffset]); + } catch (SystemException e) { + Assert.assertTrue(OffCard.getInstance().getMode() != Mode.SIM, + "SignApplet::install"); + } + + instance = applet; + } + + private SignApplet(byte[] bArray, short bOffset, byte bLength) + { + super(bArray, bOffset, bLength); + + try { + random = new SecureRandom(); + ka = KeyAgreement.getInstance("ECDH", "BC"); + ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1"); + + kpg = KeyPairGenerator.getInstance("ECDH", "BC"); + kpg.initialize(ecSpec, random); + + kp = kpg.generateKeyPair(); + + jckp = new javacard.security.KeyPair( + javacard.security.KeyPair.ALG_EC_FP, (short)SC_KEY_LENGTH); + + kf = KeyFactory.getInstance("ECDSA", "BC"); + + kp = kpg.genKeyPair(); + privKey = (ECPrivateKey)kp.getPrivate(); + pubKey = (ECPublicKey)kp.getPublic(); + + jcpubKey = (javacard.security.ECPublicKey)jckp.getPublic(); + setCurveParameters((javacard.security.ECKey)jcpubKey); + + ka.init(privKey); + signer = Signature.getInstance("SHA256withECDSA", "BC"); + jcSigner = javacard.security.Signature.getInstance( + javacard.security.Signature.ALG_ECDSA_SHA_256, false); + + } catch (NoSuchAlgorithmException | NoSuchProviderException + | InvalidAlgorithmParameterException | InvalidKeyException e) { + e.printStackTrace(); + } + } + + @Override public final boolean select() + { + if (secureChannel == null) { + secureChannel = DummyISDApplet.getInstance().getSecureChannel(); + } + secureChannel.resetSecurity(); + return true; + } + + public byte[] SELECT() + { + appletPub = null; + byte[] ret = Helper.SW6999; + byte[] result = OffCard.getInstance().select(SignApplet.class); + + ResponseAPDU response = new ResponseAPDU(result); + + if (response.getSW() == 0x9000) { + int len = result.length - 2; + byte[] remotePublicKey = new byte[len]; + Util.arrayCopyNonAtomic( + result, (short)0, remotePublicKey, (short)0, (short)(len)); + if (true == establishSecret(remotePublicKey)) { + appletPub = remotePublicKey; + ret = remotePublicKey; + } + } + + return ret; + } + + private boolean establishSecret(byte[] pubkey) + { + try { + ECPublicKeySpec cardKeySpec = new ECPublicKeySpec( + ecSpec.getCurve().decodePoint(pubkey), ecSpec); + + ECPublicKey cardKey = (ECPublicKey)kf.generatePublic(cardKeySpec); + + ka.doPhase(cardKey, true); + sharedSecret = ka.generateSecret(); + CommandAPDU command + = new CommandAPDU(0x00, + INS_ESTABLISH_SECRET, + 0, + 0, + pubKey.getQ().getEncoded(false)); + + ResponseAPDU response = OffCard.getInstance().Transmit(command); + if (response.getSW() == 0x9000) { + return true; + } + } catch (Exception e) { + // System.out.println(e.getMessage()); + } + + return false; + } + + public byte[] aid() + { + if (id_bytes == null) { + IdpassConfig cfg + = SignApplet.class.getAnnotation(IdpassConfig.class); + String strId = cfg.instanceAID(); + id_bytes = Hex.decode(strId); + } + + return id_bytes; + } + + // p1 describes the input: + // 0x00 = blob data + // 0x01 = pre-computed hash + public byte[] sign(byte[] input, int p1) + { + byte[] signature = {}; + + command = new CommandAPDU(0x00, INS_SIGN, (byte)p1, 0, input); + response = OffCard.getInstance().Transmit(command); + + if (response.getSW() != 0x9000) { + return signature; + } + + // Receive applet's signature to lastResult + lastResult = response.getData(); + + if (p1 == 0x00) { + signature = DataElement.extract(lastResult, + DataElement.TYPEDESC_SIGNATURE_D); + } else if (p1 == 0x01) { + signature = DataElement.extract(lastResult, + DataElement.TYPEDESC_SIGNATURE_H); + } + + byte[] pub + = DataElement.extract(lastResult, DataElement.TYPEDESC_PUBLICKEY); + + if (!verifySignature(p1, input, signature, pub)) { + return new byte[0]; + } + + return signature; + } + + private boolean + verifySignature(int p1, byte[] input, byte[] signature, byte[] pub) + { + if (p1 == 0x00) { + ECPublicKeySpec pubkSpec = new ECPublicKeySpec( + ecSpec.getCurve().decodePoint(pub), ecSpec); + + try { + ECPublicKey publicKey + = (ECPublicKey)kf.generatePublic(pubkSpec); + signer.initVerify(publicKey); + signer.update(input); + + return signer.verify(signature); + + } catch (InvalidKeySpecException | InvalidKeyException + | SignatureException e) { + } + + return false; + + } else if (p1 == 0x01) { + jcpubKey.setW(pub, (short)0, (short)pub.length); + jcSigner.init(jcpubKey, javacard.security.Signature.MODE_VERIFY); + + try { + return jcSigner.verifyPreComputedHash(input, + (short)0, + (short)input.length, + signature, + (short)0, + (short)signature.length); + + } catch (Exception e) { + } + + return false; + } + + return false; + } + + // This requires encrypted security level + public boolean processLoadKey(ECKeyPair keyPair) + { + boolean flag = false; + byte[] data = {}; + + DataElement privateKey + = new DataElement(DataElement.PRIVATEKEY, keyPair.getPrivateKey()); + DataElement publicKey + = new DataElement(DataElement.PUBLICKEY, keyPair.getPublicKey()); + + DataElement sequence = new DataElement(DataElement.DATSEQ); + + sequence.addElement(privateKey); + sequence.addElement(publicKey); + + data = sequence.toByteArray(); + + command = new CommandAPDU(0x00, INS_LOAD_KEYPAIR, 0, 0, data); + response = OffCard.getInstance().Transmit(command); + flag = response.getSW() == 0x9000; + + return flag; + } + + public byte[] processGetPubKey() + { + byte[] pubkey = {}; + + command = new CommandAPDU(0x00, INS_GET_PUBKEY, 0, 0); + response = OffCard.getInstance().Transmit(command); + pubkey = response.getData(); + return pubkey; + } +} diff --git a/offcard/src/main/java/org/idpass/offcard/misc/Dump.java b/offcard/src/main/java/org/idpass/offcard/misc/Dump.java new file mode 100644 index 0000000..14a5989 --- /dev/null +++ b/offcard/src/main/java/org/idpass/offcard/misc/Dump.java @@ -0,0 +1,104 @@ +package org.idpass.offcard.misc; + +import java.util.Scanner; + +public class Dump +{ + static Scanner stdin = new Scanner(System.in); + + public static void print(String title, byte[] msg, int len) + { + System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++"); + System.out.println(title); + print(msg, len); + System.out.println("-------------------------------------------------"); + } + + public static void print(byte[] msg, int len) + { + if (msg == null) + return; + + for (int j = 1; j < len + 1; j++) { + if (j % 32 == 1 || j == 0) { + if (j != 0) { + System.out.println(); + } + // System.out.format("0%d\t|\t", j / 8); + } + System.out.format("%02X", msg[j - 1]); + if (j % 4 == 0) { + System.out.print(" "); + } + } + System.out.println(); + } + + public static void print(byte[] msg, String title) + { + print(title, msg); + } + + public static void print(String title, byte[] msg) + { + System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++"); + System.out.println(title); + print(msg); + System.out.println("-------------------------------------------------"); + } + + public static void print(byte[] msg) + { + if (msg == null) + return; + + for (int j = 1; j < msg.length + 1; j++) { + if (j % 32 == 1 || j == 0) { + if (j != 0) { + System.out.println(); + } + // System.out.format("0%d\t|\t", j / 8); + } + System.out.format("%02X", msg[j - 1]); + if (j % 4 == 0) { + System.out.print(" "); + } + } + System.out.println(); + } + + public static String formatBinary(byte b) + { + String s = String.format("%8s", Integer.toBinaryString(b & 0xFF)) + .replace(' ', '0'); + return s; + } + + public static String printline(byte[] bytes, String title) + { + if (title != null) + System.out.print(String.format("%s = ", title)); + int n = 0; + StringBuilder sb = new StringBuilder(); + // sb.append("\n"); + for (byte b : bytes) { + sb.append(String.format("%02X", b)); + if (title != null) + System.out.print(String.format("%02X", b)); + n++; + /*if (n % 128 == 0) { + sb.append("\n"); + if (title!=null) System.out.println(); + }*/ + } + if (title != null) + System.out.println(); + return sb.toString(); + } + + public static String input(String msg) + { + System.out.println(msg); + return stdin.next(); + } +} diff --git a/offcard/src/main/java/org/idpass/offcard/misc/Helper.java b/offcard/src/main/java/org/idpass/offcard/misc/Helper.java new file mode 100644 index 0000000..294a07b --- /dev/null +++ b/offcard/src/main/java/org/idpass/offcard/misc/Helper.java @@ -0,0 +1,308 @@ +package org.idpass.offcard.misc; + +import java.io.UnsupportedEncodingException; +import java.util.List; +import java.util.Random; +import java.util.UUID; + +import javax.smartcardio.Card; +import javax.smartcardio.CardChannel; +import javax.smartcardio.CardException; +import javax.smartcardio.CardTerminal; +import javax.smartcardio.CardTerminals; +import javax.smartcardio.TerminalFactory; + +import org.idpass.offcard.proto.SCP02; +// import org.testng.Assert; + +import com.licel.jcardsim.smartcardio.CardSimulator; +import com.licel.jcardsim.smartcardio.CardTerminalSimulator; + +import javacard.framework.Util; + +import org.globalplatform.SecureChannel; + +// clang-format off + +public class Helper +{ + public static final String SHORT_UUID_BASE = "000000000000000000DEC0DE"; + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + private static Invariant Assert = new Invariant(true); + + public static CardSimulator simulator; + public static CardChannel channel; + + public static final byte[] SW9000 = new byte[] {(byte)0x90, (byte)0x00}; + public static final byte[] SW9100 = new byte[] {(byte)0x91, (byte)0x00}; + public static final byte[] SW6A88 = new byte[] {(byte)0x6A, (byte)0x88}; // Reference data not found + public static final byte[] SW6985 = new byte[] {(byte)0x69, (byte)0x85}; + public static final byte[] SW6999 = new byte[] {(byte)0x69, (byte)0x99}; // SW_APPLET_SELECT_FAILED + public static final byte[] SW6701 = new byte[] {(byte)0x67, (byte)0x01}; + + public static final int SW_NO_ERROR = 0x9000; + public static final int SW_NO_PRECISE_DIAGNOSIS = 0x6F00; + public static final int SW_KEY_NOT_FOUND = 0x6A88; + public static final int SW_RECORD_NOT_FOUND = 0x6A83; + public static final int SW_VERIFICATION_FAILED = 0x6300; + + private static Random ran = new Random(); + + public enum Mode { SIM, PHY } + // clang-format on + + public static void reInitialize() + { + channel = null; + simulator = null; + } + + public static String printsL(byte sL) + { + String s = ""; + + if (sL == SecureChannel.NO_SECURITY_LEVEL) { + s = "NO_SECURITY_LEVEL"; + } + + if ((sL & SecureChannel.C_MAC) != 0) { + s = s + "C_MAC"; + } + + if ((sL & SecureChannel.C_DECRYPTION) != 0) { + s = s + "|C_DECRYPTION"; + } + + if ((sL & SecureChannel.R_MAC) != 0) { + s = s + "|R_MAC"; + } + + if ((sL & SecureChannel.R_ENCRYPTION) != 0) { + s = s + "|C_ENCRYPTION"; + } + + if ((sL & SCP02.ANY_AUTHENTICATED) != 0) { + s = s + "|ANY_AUTHENTICATED"; + } + + if ((sL & SecureChannel.AUTHENTICATED) != 0) { + s = s + "|AUTHENTICATED"; + } + + return s; + } + + // Kvno = Key Version Number as termed in the spec + // This method simulates the card defaulting to + // different kvno. Since 0xFF is a valid + // kvno to mean an NXP factory default key, that value + // is also included. If a list has 5 elements, this + // method randomly returns any of: 1 to 5 inclusive, 0xFF + public static int getRandomKvno(int n) + { + int lower = 1; + int upper = lower + n - 1; + int r = ran.nextInt(upper + 1) + + lower; // between lower and upper inclusive or 0xFF + if (r > upper) { + r = 0xFF; + } + return r; + } + + public static String print(byte[] bytes) + { + int n = 0; + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + for (byte b : bytes) { + sb.append(String.format("%02X ", b)); + n++; + if (n % 32 == 0) { + sb.append("\n"); + } + } + return sb.toString(); + } + + public static byte[] arrayConcat(byte[] arr1, byte[] arr2) + { + byte[] arr1arr2 = new byte[arr1.length + arr2.length]; + System.arraycopy(arr1, 0, arr1arr2, 0, arr1.length); + System.arraycopy(arr2, 0, arr1arr2, arr1.length, arr2.length); + return arr1arr2; + } + + public static CardChannel getPcscChannel() // throws CardException + { + if (channel != null) { + return channel; + } + + TerminalFactory factory = TerminalFactory.getDefault(); + + try { + CardTerminals terms = factory.terminals(); + if (terms != null) { + List terminals = terms.list(); + int n = terminals.size(); + if (n == 2 || n == 4) { + CardTerminal terminal = terminals.get(3); + Card card = null; + card = terminal.connect("*"); + channel = card.getBasicChannel(); + } + } + } catch (CardException e) { + System.out.println(e.getCause()); + } catch (IndexOutOfBoundsException e) { + System.out.println(e.getCause()); + } + + return channel; + } + + public static CardChannel getjcardsimChannel() throws CardException + { + if (channel != null) { + return channel; + } + + simulator = new CardSimulator(); + CardTerminal terminal = CardTerminalSimulator.terminal(simulator); + Card card = terminal.connect("T=1"); + channel = card.getBasicChannel(); + return channel; + } + + public static boolean checkstatus(byte[] byteseq) + { + byte[] status = new byte[2]; + if (byteseq.length < 2) { + return false; + } + Util.arrayCopyNonAtomic(byteseq, + (short)(byteseq.length - 2), + status, + (short)0, + (short)status.length); + return java.util.Arrays.equals(status, Helper.SW9000); + } + + public static String bytesToHex(byte[] bytes) + { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } + + public static byte[] getASCIIBytes(String str) + { + try { + return str.getBytes("US-ASCII"); + } catch (IllegalArgumentException e) { + return str.getBytes(); + } catch (UnsupportedEncodingException e) { + return str.getBytes(); + } + } + + public static byte[] clone(byte[] value) + { + if (value == null) { + return null; + } + int length = ((byte[])value).length; + byte[] bClone = new byte[length]; + System.arraycopy(value, 0, bClone, 0, length); + return bClone; + } + + public static long UUIDTo32Bit(UUID uuid) + { + if (uuid == null) { + return -1; + } + String str = uuid.toString().toUpperCase(); + int shortIdx = str.indexOf(SHORT_UUID_BASE); + if ((shortIdx != -1) + && (shortIdx + SHORT_UUID_BASE.length() == str.length())) { + // This is short 16-bit or 32-bit UUID + return Long.parseLong(str.substring(0, shortIdx), 16); + } + return -1; + } + + public static byte[] UUIDToByteArray(String uuidStringValue) + { + byte[] uuidValue = new byte[16]; + if (uuidStringValue.indexOf('-') != -1) { + /* + throw new NumberFormatException( + "The '-' character is not allowed in UUID: " + + uuidStringValue);*/ + uuidStringValue = uuidStringValue.replaceAll("[\\s\\-()]", ""); + } + for (int i = 0; i < 16; i++) { + uuidValue[i] = (byte)Integer.parseInt( + uuidStringValue.substring(i * 2, i * 2 + 2), 16); + } + return uuidValue; + } + + public static byte[] UUIDToByteArray(final UUID uuid) + { + return UUIDToByteArray(uuid.toString()); + } + + public static String newStringUTF8(byte bytes[]) + { + try { + return new String(bytes, "UTF-8"); + } catch (IllegalArgumentException e) { + return new String(bytes); + } catch (UnsupportedEncodingException e) { + return new String(bytes); + } + } + + public static String newStringASCII(byte bytes[]) + { + try { + return new String(bytes, "US-ASCII"); + } catch (IllegalArgumentException e) { + return new String(bytes); + } catch (UnsupportedEncodingException e) { + return new String(bytes); + } + } + + public static String toHexString(long l) + { + StringBuffer buf = new StringBuffer(); + String lo = Integer.toHexString((int)l); + if (l > 0xffffffffl) { + String hi = Integer.toHexString((int)(l >> 32)); + buf.append(hi); + for (int i = lo.length(); i < 8; i++) { + buf.append('0'); + } + } + buf.append(lo); + return buf.toString(); + } + + public static String UUIDByteArrayToString(byte[] uuidValue) + { + StringBuffer buf = new StringBuffer(); + for (int i = 0; i < uuidValue.length; i++) { + buf.append(Integer.toHexString(uuidValue[i] >> 4 & 0xf)); + buf.append(Integer.toHexString(uuidValue[i] & 0xf)); + } + return buf.toString(); + } +} diff --git a/offcard/src/main/java/org/idpass/offcard/misc/IdpassConfig.java b/offcard/src/main/java/org/idpass/offcard/misc/IdpassConfig.java new file mode 100644 index 0000000..97a067c --- /dev/null +++ b/offcard/src/main/java/org/idpass/offcard/misc/IdpassConfig.java @@ -0,0 +1,18 @@ +package org.idpass.offcard.misc; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface IdpassConfig { + String packageAID() default ""; + String appletAID() default ""; + String instanceAID() default ""; + String capFile() default ""; + byte[] privileges(); + byte[] installParams(); + Class api() default Void.class; +} diff --git a/offcard/src/main/java/org/idpass/offcard/misc/Invariant.java b/offcard/src/main/java/org/idpass/offcard/misc/Invariant.java new file mode 100644 index 0000000..396696d --- /dev/null +++ b/offcard/src/main/java/org/idpass/offcard/misc/Invariant.java @@ -0,0 +1,82 @@ +package org.idpass.offcard.misc; + +import org.testng.asserts.IAssert; +import org.testng.asserts.SoftAssert; + +// This is a soft Assert object that prints if +// an assertion fails but continues execution +public class Invariant extends SoftAssert +{ + private boolean iflag = false; // for local control + private boolean cflag = false; // for global control + private static int errorCount; + + public static boolean check() + { + if (errorCount != 0) { + System.out.println("*** Invariant errorCount = " + errorCount + + " ***"); + } else + System.out.println("--- Invariant OK ---"); + return errorCount == 0; + } + + public Invariant(boolean flag) + { + cflag = System.getProperty("xxx") != null ? true : false; + this.iflag = flag; + } + + public Invariant() + { + cflag = System.getProperty("xxx") != null ? true : false; + iflag = false; + } + + @Override + public void onAssertFailure(IAssert assertCommand, AssertionError ex) + { + errorCount++; + + String m = ex.getMessage(); + int idx = m.indexOf("expected"); + + Object expected = assertCommand.getExpected(); + Object actual = assertCommand.getActual(); + String title = m.substring(0, idx); + String msg = String.format("AssertionError@( %s) ", title); + + String exp = "?"; + String act = "?"; + + if (expected instanceof Integer || expected instanceof Byte + || expected instanceof Short) { + exp = String.format("0x%04X", + Integer.parseInt(expected.toString())); + act = String.format("0x%04X", Integer.parseInt(actual.toString())); + System.out.println( + String.format("%s expecting %s, got %s", msg, exp, act)); + } else if (expected instanceof byte[]) { + byte[] exp_bytes = (byte[])expected; + byte[] act_bytes = (byte[])actual; + System.out.println(ex.getMessage()); + Dump.print("Expected bytes:", exp_bytes); + Dump.print("Received bytes:", act_bytes); + } else { + if (expected != null) { + exp = expected.toString(); + } + if (actual != null) { + act = actual.toString(); + } + + System.out.println( + String.format("%s expecting object %s, got %s", msg, exp, act)); + } + + if (iflag || cflag) { + // throw new AssertionError(msg); + throw new IllegalStateException(msg); + } + } +} diff --git a/offcard/src/main/java/org/idpass/offcard/proto/CryptoAPI.java b/offcard/src/main/java/org/idpass/offcard/proto/CryptoAPI.java new file mode 100644 index 0000000..6f08e74 --- /dev/null +++ b/offcard/src/main/java/org/idpass/offcard/proto/CryptoAPI.java @@ -0,0 +1,414 @@ +package org.idpass.offcard.proto; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.BadPaddingException; + +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.InvalidKeyException; +import java.security.InvalidAlgorithmParameterException; +import java.util.Arrays; +import java.security.Key; +import java.security.GeneralSecurityException; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.security.Security; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.ECPoint; + +public class CryptoAPI +{ + static + { + Security.addProvider(new BouncyCastleProvider()); + } + + public static final byte[] constENC = new byte[] {(byte)0x01, (byte)0x82}; + public static final byte[] constMAC = new byte[] {(byte)0x01, (byte)0x01}; + public static final byte[] constDEK = new byte[] {(byte)0x01, (byte)0x81}; + + public static final byte[] NullBytes8 + = new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + static final IvParameterSpec iv_null_8 = new IvParameterSpec(NullBytes8); + + public static void init() + { + Security.addProvider(new BouncyCastleProvider()); + } + + public static byte[] deriveSCP02SessionKey(byte[] cardKey, + byte[] seq, + byte[] purposeData) + { + byte[] key24 = resizeDES(cardKey, 24); + + try { + byte[] derivationData = new byte[16]; + // 2 bytes constant + System.arraycopy(purposeData, 0, derivationData, 0, 2); + // 2 bytes sequence counter + 12 bytes 0x00 + System.arraycopy(seq, 0, derivationData, 2, 2); + + SecretKeySpec tmpKey = new SecretKeySpec(key24, "DESede"); + + Cipher cipher = Cipher.getInstance("DESede/CBC/NoPadding", "BC"); + cipher.init( + Cipher.ENCRYPT_MODE, tmpKey, new IvParameterSpec(NullBytes8)); + + return cipher.doFinal(derivationData); + + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalStateException("error generating session keys.", + e); + } catch (InvalidKeyException | IllegalBlockSizeException + | BadPaddingException | InvalidAlgorithmParameterException e) { + throw new RuntimeException("error generating session keys.", e); + } catch (NoSuchProviderException e) { + throw new RuntimeException("SpongyCastle not installed"); + } + } + + public static byte[] resizeDES(byte[] key, int length) + { + if (length == 24) { + byte[] key24 = new byte[24]; + System.arraycopy(key, 0, key24, 0, 16); + System.arraycopy(key, 0, key24, 16, 8); + return key24; + } else { + byte[] key8 = new byte[8]; + System.arraycopy(key, 0, key8, 0, 8); + return key8; + } + } + + public static byte[] calcCryptogram(byte[] text, byte[] sENC) + { + byte[] d = pad80(text, 8); + Key key24 = new SecretKeySpec(resizeDES(sENC, 24), "DESede"); + + try { + Cipher cipher = Cipher.getInstance("DESede/CBC/NoPadding"); + cipher.init( + Cipher.ENCRYPT_MODE, key24, new IvParameterSpec(NullBytes8)); + byte[] result = new byte[8]; + byte[] res = cipher.doFinal(d, 0, d.length); // -des-ede-cbc + // byte[] res = cipher.doFinal(text, 0, text.length); // + // -des-ede-cbc + System.arraycopy(res, res.length - 8, result, 0, 8); + return result; + } catch (GeneralSecurityException e) { + throw new RuntimeException("MAC computation failed.", e); + } + } + + // byte[] mac = computeMAC(apdu,initV,k); + public static byte[] computeMAC(byte[] data, byte[] icv, byte[] sMAC) + { + byte[] dataPadded = pad80(data, 8); + + try { + Cipher cipher1 = Cipher.getInstance("DES/CBC/NoPadding", "BC"); + cipher1.init(Cipher.ENCRYPT_MODE, + new SecretKeySpec(resizeDES(sMAC, 8), "DES"), + new IvParameterSpec(icv)); + Cipher cipher2 = Cipher.getInstance("DESede/CBC/NoPadding", "BC"); + cipher2.init(Cipher.ENCRYPT_MODE, + new SecretKeySpec(resizeDES(sMAC, 24), "DESede"), + new IvParameterSpec(icv)); + + byte[] result = new byte[8]; + byte[] temp; + + if (dataPadded.length > 8) { + // doFinal(byte[] input, int inputOffset, int inputLen) + temp = cipher1.doFinal( + dataPadded, 0, dataPadded.length - 8); // -des-cbc + System.arraycopy(temp, temp.length - 8, result, 0, 8); + // --------------- + // ^ + // |_ move pointer to last 8 bytes + cipher2.init(Cipher.ENCRYPT_MODE, + new SecretKeySpec(resizeDES(sMAC, 24), "DESede"), + new IvParameterSpec(result)); + } + byte[] t = new byte[8]; + System.arraycopy(dataPadded, (0 + dataPadded.length) - 8, t, 0, 8); + temp = cipher2.doFinal( + dataPadded, (0 + dataPadded.length) - 8, 8); // -des-ede-cbc + System.arraycopy(temp, temp.length - 8, result, 0, 8); + return result; + } catch (GeneralSecurityException e) { + throw new RuntimeException("MAC computation failed.", e); + } + } + + public static byte[] pad80(byte[] text, int blocksize) + { + int total = (text.length / blocksize + 1) * blocksize; + byte[] result = Arrays.copyOfRange(text, 0, total); + result[text.length] = (byte)0x80; + return result; + } + + public static byte[] unpad80(byte[] text) + { + byte[] result = {}; + + if (text.length < 1) { + // throw new BadPaddingException("Invalid ISO 7816-4 padding"); + return result; + } + int offset = text.length - 1; + while (offset > 0 && text[offset] == 0) { + offset--; + } + if (text[offset] != (byte)0x80) { + // throw new BadPaddingException("Invalid ISO 7816-4 padding"); + return result; + } + result = Arrays.copyOf(text, offset); + + return result; + } + + public static byte[] updateIV(byte[] prevIV, byte[] sMAC) + { + try { + byte[] k8 = resizeDES(sMAC, 8); + Cipher c = Cipher.getInstance("DES/ECB/NoPadding", "BC"); + SecretKeySpec keyspec = new SecretKeySpec(k8, "DES"); + c.init(Cipher.ENCRYPT_MODE, keyspec); + + byte[] newIv = c.doFinal(prevIV); // -des-ecb + return newIv; + } catch (GeneralSecurityException e) { + throw new RuntimeException("computation failed.", e); + } + } + + /* + # better to display by od: + openssl enc -des-ede-cbc \ + -K $sENC${sENC:0:16} \ + -iv 0000000000000000 \ + -in ~/helloworld_pad80.hex | od + */ + public static byte[] encryptData(byte[] data, byte[] sENC) + { + byte[] padded = pad80(data, 8); + + try { + Cipher c = Cipher.getInstance("DESede/CBC/NoPadding"); + Key k = new SecretKeySpec(resizeDES(sENC, 24), "DESede"); + c.init(Cipher.ENCRYPT_MODE, k, new IvParameterSpec(NullBytes8)); + return c.doFinal(padded); // -des-ede-cbc + } catch (GeneralSecurityException e) { + throw new RuntimeException("error: encryptData failed", e); + } + } + + /** + * Decrypts the response from the card using the session key. The returned + * data is already stripped from IV and padding and can be potentially + * empty. + * + * @param data the ciphetext + * @return the plaintext + */ + public static byte[] decryptData(byte[] data, byte[] sENC) + { + try { + Cipher c = Cipher.getInstance("DESede/CBC/NoPadding"); + Key k = new SecretKeySpec(resizeDES(sENC, 24), "DESede"); + IvParameterSpec ivParameterSpec = new IvParameterSpec(NullBytes8); + c.init(Cipher.DECRYPT_MODE, k, ivParameterSpec); + byte[] x = c.doFinal(data); + byte[] unpaddedx = unpad80(x); + return unpaddedx; + } catch (GeneralSecurityException e) { + throw new RuntimeException("Is BouncyCastle in the classpath?", e); + } + } + + ///////////////// byte[] <==> ECPublicKey,ECPrivateKey //////////////////// + public static byte[] fromECPrivateKey(javacard.security.ECPrivateKey key) + { + byte[] byteseq = {}; + + if (key.isInitialized()) { + short n = (short)(key.getSize() / 8); + byteseq = new byte[n]; + short retval = key.getS(byteseq, (short)0); + // Assert.assertEquals(retval,n, "javacard ECPrivateKey len + // anomaly"); + return byteseq; + } + + return byteseq; + } + + public static byte[] fromECPublicKey(javacard.security.ECPublicKey key) + { + byte[] byteseq = {}; + + if (key.isInitialized()) { + short n = (short)(key.getSize() / 8); + n = 65; // always 65 + byteseq = new byte[n]; + short retval = key.getW(byteseq, (short)0); + // Assert.assertEquals(retval,n, "javacard ECPublicKey len 65 + // anomaly"); + return byteseq; + } + + return byteseq; + } + + public static byte[] fromECPublicKey( + java.security.interfaces.ECPublicKey key) + { + int keyLengthBytes = key.getParams().getOrder().bitLength() / Byte.SIZE; + byte[] publkeybytes = new byte[2 * keyLengthBytes]; + + int offset = 0; + + BigInteger x = key.getW().getAffineX(); + byte[] xba = x.toByteArray(); + if (xba.length > keyLengthBytes + 1 + || xba.length == keyLengthBytes + 1 && xba[0] != 0) { + throw new IllegalStateException( + "X coordinate of EC public key has wrong size"); + } + + if (xba.length == keyLengthBytes + 1) { + System.arraycopy(xba, 1, publkeybytes, offset, keyLengthBytes); + } else { + System.arraycopy(xba, + 0, + publkeybytes, + offset + keyLengthBytes - xba.length, + xba.length); + } + offset += keyLengthBytes; + + BigInteger y = key.getW().getAffineY(); + byte[] yba = y.toByteArray(); + if (yba.length > keyLengthBytes + 1 + || yba.length == keyLengthBytes + 1 && yba[0] != 0) { + throw new IllegalStateException( + "Y coordinate of EC public key has wrong size"); + } + + if (yba.length == keyLengthBytes + 1) { + System.arraycopy(yba, 1, publkeybytes, offset, keyLengthBytes); + } else { + System.arraycopy(yba, + 0, + publkeybytes, + offset + keyLengthBytes - yba.length, + yba.length); + } + + return publkeybytes; + } + + public static byte[] fromECPrivateKey( + java.security.interfaces.ECPrivateKey key) + { + int keyLengthBytes = key.getParams().getOrder().bitLength() / Byte.SIZE; + byte[] privkeybytes = new byte[keyLengthBytes]; + int offset = 0; + + BigInteger x = key.getS(); + byte[] xba = x.toByteArray(); + if (xba.length > keyLengthBytes + 1 + || xba.length == keyLengthBytes + 1 && xba[0] != 0) { + throw new IllegalStateException("ERROR"); + } + + if (xba.length == keyLengthBytes + 1) { + System.arraycopy(xba, 1, privkeybytes, offset, keyLengthBytes); + } else { + System.arraycopy(xba, + 0, + privkeybytes, + offset + keyLengthBytes - xba.length, + xba.length); + } + + return privkeybytes; + } + + /* + public static void secret(byte[] b) + { + try { + CryptoAPI.init(); + + KeyPairGenerator kpgen; + kpgen = KeyPairGenerator.getInstance("ECDH", "BC"); + + ECGenParameterSpec genspec = new ECGenParameterSpec("secp256k1"); + kpgen.initialize(genspec); + + java.security.KeyPair localKeyPair = kpgen.generateKeyPair(); + // java.security.KeyPair remoteKeyPair = kpgen.generateKeyPair(); + + _o.o_(b); + + // test creation + ECPublicKey remoteKey = constructECPublicKey( + ((ECPublicKey)localKeyPair.getPublic()).getParams(), b); + + // local key agreement + javax.crypto.KeyAgreement localKA + = javax.crypto.KeyAgreement.getInstance("ECDH"); + localKA.init(localKeyPair.getPrivate()); + localKA.doPhase(remoteKey, false); + byte[] localSecret = localKA.generateSecret(); + + _o.o_(localSecret); + + } catch (NoSuchAlgorithmException | NoSuchProviderException + | InvalidAlgorithmParameterException | InvalidKeySpecException + | InvalidKeyException e) { + e.printStackTrace(); + } + } + */ + + public static java.security.interfaces.ECPublicKey + constructECPublicKey(java.security.spec.ECParameterSpec params, + byte[] pubkey) + throws NoSuchAlgorithmException, InvalidKeySpecException + { + int keySizeBytes = params.getOrder().bitLength() / Byte.SIZE; + + int offset = 0; + BigInteger x = new BigInteger( + 1, Arrays.copyOfRange(pubkey, offset, offset + keySizeBytes)); + offset += keySizeBytes; + BigInteger y = new BigInteger( + 1, Arrays.copyOfRange(pubkey, offset, offset + keySizeBytes)); + ECPoint w = new ECPoint(x, y); + + ECPublicKeySpec otherKeySpec = new ECPublicKeySpec(w, params); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + ECPublicKey otherKey + = (ECPublicKey)keyFactory.generatePublic(otherKeySpec); + + return otherKey; + } +} diff --git a/offcard/src/main/java/org/idpass/offcard/proto/DataElement.java b/offcard/src/main/java/org/idpass/offcard/proto/DataElement.java new file mode 100644 index 0000000..94f7138 --- /dev/null +++ b/offcard/src/main/java/org/idpass/offcard/proto/DataElement.java @@ -0,0 +1,1784 @@ +/** + * BlueCove - Java library for Bluetooth + * + * Java docs licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * (c) Copyright 2001, 2002 Motorola, Inc. ALL RIGHTS RESERVED. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * @version $Id$ + */ +package org.idpass.offcard.proto; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.Vector; + +import org.idpass.offcard.misc.Helper; + +//clang-format off +// Type Descriptor Table +/*------------------+-----------------------------------------------+ +| | Valid | | +| Type Descriptor | Size Descriptor | Description | +|---|---|---|---|---|-----------------|-----------------------------| +| 0 | 0 | 0 | 0 | 0 | 0 | Nil. The special null type | +|---|---|---|---|---|-----------------|-----------------------------| +| 0 | 0 | 0 | 0 | 1 | 0,1,2,3,4 | Unsigned integer | +|---|---|---|---|---|-----------------|-----------------------------| +| 0 | 0 | 0 | 1 | 0 | 0,1,2,3,4 | Signed 2s complement integer| +|---|---|---|---|---|-----------------|-----------------------------| +| 0 | 0 | 0 | 1 | 1 | 1,2,4 | UUID | +|---|---|---|---|---|-----------------|-----------------------------| +| 0 | 0 | 1 | 0 | 0 | 5,6,7 | Text string | +|---|---|---|---|---|-----------------|-----------------------------| +| 0 | 0 | 1 | 0 | 1 | 0 | Boolean | +|---|---|---|---|---|-----------------|-----------------------------| +| 0 | 0 | 1 | 1 | 0 | 5,6,7 | Data Element Sequence | +|---|---|---|---|---|-----------------|-----------------------------| +| 0 | 0 | 1 | 1 | 1 | 5,6,7 | Data Element Alternative | +|---|---|---|---|---|-----------------|-----------------------------| +| 0 | 1 | 0 | 0 | 0 | 5,6,7 | URI, uniform resource loc | +|---|---|---|---|---|-----------------|-----------------------------| +| 0 | 1 | 0 | 0 | 1 | 5 | EC private key | +|---|---|---|---|---|-----------------|-----------------------------| +| 0 | 1 | 0 | 1 | 0 | 5 | EC public key | +|-------------------|-----------------|-----------------------------| +| 0 | 1 | 0 | 1 | 1 | 5 | SignatureD (data) | +|-------------------|-----------------|-----------------------------| +| 0 | 1 | 1 | 0 | 0 | 5 | SignatureH (hash) | +|-------------------|-----------------|-----------------------------| +| 11-30 | | 18 more custom types here | +|-------------------|-----------------|-----------------------------| +| 1 | 1 | 1 | 1 | 1 | 0 | The type descriptor is | +| | | | | | | delegated to the next byte | +|-------------------------------------|--------------------------- */ + +// Size Descriptor Table +/*----------+-------------------------------------------------------+ +| | | | +| Size Index| Additional bits | Data size | +|---|---|---|-----------------|-------------------------------------| +| 0 | 0 | 0 | 0 | 1 byte. Exception if data element is| +| | | | | nil then the data size is 0 byte | +|---|---|---|-----------------|-------------------------------------| +| 0 | 0 | 1 | 0 | 2 bytes | +|---|---|---|-----------------|-------------------------------------| +| 0 | 1 | 0 | 0 | 4 bytes | +|---|---|---|-----------------|-------------------------------------| +| 0 | 1 | 1 | 0 | 8 bytes | +|---|---|---|-----------------|-------------------------------------| +| 1 | 0 | 0 | 0 | 16 bytes | +|---|---|---|-----------------|-------------------------------------| +| 1 | 0 | 1 | 8 | The data size is contained in the | +| | | | | additional 8 bits as unsigned int | +|---|---|---|-----------------|-------------------------------------| +| 1 | 1 | 0 | 16 | The data size is contained in the | +| | | | | additional 16 bits as unsigned int | +|---|---|---|-----------------|-------------------------------------| +| 1 | 1 | 1 | 32 | The data size is contained in the | +| | | | | additional 32 bits as usgigned int | +|------------------------------------------------------------------ */ +//clang-format on + +/** + * The DataElement class defines the various data types that a + * Bluetooth service attribute value may have. + * + * The following table describes the data types and valid values that a + * DataElement object can store. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Data TypeValid Values
NULLrepresents a null value
U_INT_1 + * long value range [0, 255]
U_INT_2long value range [0, 216-1]
U_INT_4long value range [0, 232-1]
U_INT_8byte[] value range [0, 264-1]
U_INT_16byte[] value range [0, 2128-1]
INT_1long value range [-128, 127]
INT_2long value range [-215, 215-1]
INT_4long value range [-231, 231-1]
INT_8long value range [-263, 263-1]
INT_16byte[] value range [-2127, + * 2127-1]
URLjava.lang.String
UUIDjavax.bluetooth.UUID
BOOLboolean
STRINGjava.lang.String
DATSEQjava.util.Enumeration
DATALTjava.util.Enumeration
+ * + * + */ + +public class DataElement +{ + /* + * The following section defines public, static and instance member + * variables used in the implementation of the methods. + */ + + /** + * Defines data of type NULL. + * + * The value for data type DataElement.NULL is implicit, + * i.e., there is no representation of it. Accordingly there is no method to + * retrieve it, and attempts to retrieve the value will throw an exception. + *

+ * The value of NULL is 0x00 (0). + * + */ + public static final int NULL = 0x0000; + + /** + * Defines an unsigned integer of size one byte. + *

+ * The value of the constant U_INT_1 is 0x08 (8). + */ + public static final int U_INT_1 = 0x0008; + + /** + * Defines an unsigned integer of size two bytes. + *

+ * The value of the constant U_INT_2 is 0x09 (9). + */ + public static final int U_INT_2 = 0x0009; + + /** + * Defines an unsigned integer of size four bytes. + *

+ * The value of the constant U_INT_4 is 0x0A (10). + */ + public static final int U_INT_4 = 0x000A; + + /** + * Defines an unsigned integer of size eight bytes. + *

+ * The value of the constant U_INT_8 is 0x0B (11). + */ + public static final int U_INT_8 = 0x000B; + + /** + * Defines an unsigned integer of size sixteen bytes. + *

+ * The value of the constant U_INT_16 is 0x0C (12). + */ + public static final int U_INT_16 = 0x000C; + + /** + * Defines a signed integer of size one byte. + *

+ * The value of the constant INT_1 is 0x10 (16). + */ + public static final int INT_1 = 0x0010; + + /** + * Defines a signed integer of size two bytes. + *

+ * The value of the constant INT_2 is 0x11 (17). + */ + public static final int INT_2 = 0x0011; + + /** + * Defines a signed integer of size four bytes. + *

+ * The value of the constant INT_4 is 0x12 (18). + */ + public static final int INT_4 = 0x0012; + + /** + * Defines a signed integer of size eight bytes. + *

+ * The value of the constant INT_8 is 0x13 (19). + */ + public static final int INT_8 = 0x0013; + + /** + * Defines a signed integer of size sixteen bytes. + *

+ * The value of the constant INT_16 is 0x14 (20). + */ + public static final int INT_16 = 0x0014; + + /** + * Defines data of type URL. + *

+ * The value of the constant URL is 0x40 (64). + */ + public static final int URL = 0x0040; + + /** + * Defines data of type UUID. + *

+ * The value of the constant UUID is 0x18 (24). + */ + public static final int UUID = 0x0018; + + /** + * Defines data of type BOOL. + *

+ * The value of the constant BOOL is 0x28 (40). + */ + public static final int BOOL = 0x0028; + + /** + * Defines data of type STRING. + *

+ * The value of the constant STRING is 0x20 (32). + */ + public static final int STRING = 0x0020; + + /** + * Defines data of type DATSEQ. The service attribute value whose data has + * this type must consider all the elements of the list, i.e. the value is + * the whole set and not a subset. The elements of the set can be of any + * type defined in this class, including DATSEQ. + *

+ * The value of the constant DATSEQ is 0x30 (48). + */ + public static final int DATSEQ = 0x0030; + + /** + * Defines data of type DATALT. The service attribute value whose data has + * this type must consider only one of the elements of the set, i.e., the + * value is the not the whole set but only one element of the set. The user + * is free to choose any one element. The elements of the set can be of any + * type defined in this class, including DATALT. + *

+ * The value of the constant DATALT is 0x38 (56). + */ + public static final int DATALT = 0x0038; + + public static final int PRIVATEKEY = 0x0039; + public static final int PUBLICKEY = 0x003A; + public static final int SIGNATURE_D = 0x003B; + public static final int SIGNATURE_H = 0x003C; + + static byte[] validHeaders = { + (byte)0x00, (byte)0x08, (byte)0x09, (byte)0x0A, (byte)0x0B, (byte)0x0C, + (byte)0x10, (byte)0x11, (byte)0x12, (byte)0x13, (byte)0x14, (byte)0x19, + (byte)0x1A, (byte)0x1C, (byte)0x25, (byte)0x26, (byte)0x28, (byte)0x35, + (byte)0x36, (byte)0x3D, (byte)0x3E, (byte)0x45, (byte)0x46, (byte)0x4D, + (byte)0x55, (byte)0x5D, (byte)0x65}; + + public static final byte TYPEDESC_NULL = 0x00; + public static final byte TYPEDESC_INT_1 = 0x02; // differs in sizeDesc + public static final byte TYPEDESC_INT_2 = 0x02; // differs in sizeDesc + public static final byte TYPEDESC_DATASEQ = 0x06; + public static final byte TYPEDESC_DATALT = 0x07; + public static final byte TYPEDESC_PRIVATEKEY = 0x09; + public static final byte TYPEDESC_PUBLICKEY = 0x0A; + public static final byte TYPEDESC_SIGNATURE_D = 0x0B; + public static final byte TYPEDESC_SIGNATURE_H = 0x0C; + + private Object value; + + private int valueType; + + private ByteArrayOutputStream out; + private InputStream in; + private int pos; + + /** + * Creates a DataElement of type NULL, + * DATALT, or DATSEQ. + * + * @see #NULL + * @see #DATALT + * @see #DATSEQ + * + * @param valueType + * the type of DataElement to create: NULL, + * DATALT, or DATSEQ + * + * @exception IllegalArgumentException + * if valueType is not NULL, + * DATALT, or DATSEQ + */ + + public DataElement(int valueType) + { + switch (valueType) { + case NULL: + value = null; + break; + case DATALT: + case DATSEQ: + value = new Vector(); + break; + default: + throw new IllegalArgumentException( + "valueType " + typeToString(valueType) + + " is not DATSEQ, DATALT or NULL"); + } + + this.valueType = valueType; + } + + /** + * Creates a DataElement whose data type is BOOL + * and whose value is equal to bool + * + * @see #BOOL + * + * @param bool + * the value of the DataElement of type BOOL. + */ + + public DataElement(boolean bool) + { + value = bool ? Boolean.TRUE : Boolean.FALSE; + valueType = BOOL; + } + + /** + * Creates a DataElement that encapsulates an integer value + * of size U_INT_1, U_INT_2, + * U_INT_4, INT_1, INT_2, + * INT_4, and INT_8. The legal values for + * the valueType and the corresponding attribute values are: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Value TypeValue Range
U_INT_1[0, 28-1]
U_INT_2[0, 216-1]
U_INT_4[0, 232-1]
INT_1[-27, 27-1]
INT_2[-215, 215-1]
INT_4[-231, 231-1]
INT_8[-263, 263-1]
All other pairings are illegal and will cause an + * IllegalArgumentException to be thrown. + * + * @see #U_INT_1 + * @see #U_INT_2 + * @see #U_INT_4 + * @see #INT_1 + * @see #INT_2 + * @see #INT_4 + * @see #INT_8 + * + * @param valueType + * the data type of the object that is being created; must be one + * of the following: U_INT_1, + * U_INT_2, U_INT_4, + * INT_1, INT_2, + * INT_4, or INT_8 + * + * @param value + * the value of the object being created; must be in the range + * specified for the given valueType + * + * @exception IllegalArgumentException + * if the valueType is not valid or the + * value for the given legal + * valueType is outside the valid range + * + */ + + public DataElement(int valueType, long value) + { + switch (valueType) { + case U_INT_1: + if (value < 0 || value > 0xff) { + throw new IllegalArgumentException(value + " not U_INT_1"); + } + break; + case U_INT_2: + if (value < 0 || value > 0xffff) { + throw new IllegalArgumentException(value + " not U_INT_2"); + } + break; + case U_INT_4: + if (value < 0 || value > 0xffffffffl) { + throw new IllegalArgumentException(value + " not U_INT_4"); + } + break; + case INT_1: + if (value < -0x80 || value > 0x7f) { + throw new IllegalArgumentException(value + " not INT_1"); + } + break; + case INT_2: + if (value < -0x8000 || value > 0x7fff) { + throw new IllegalArgumentException(value + " not INT_2"); + } + break; + case INT_4: + if (value < -0x80000000 || value > 0x7fffffff) { + throw new IllegalArgumentException(value + " not INT_4"); + } + break; + case INT_8: + // Not boundaries tests + break; + default: + throw new IllegalArgumentException("type " + typeToString(valueType) + + " can't be represented long"); + } + + this.value = new Long(value); + this.valueType = valueType; + } + + /** + * Creates a DataElement whose data type is given by + * valueType and whose value is specified by the argument + * value. The legal values for the valueType + * and the corresponding attribute values are: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Value TypeJava Type / Value Range
URLjava.lang.String
UUIDjavax.bluetooth.UUID
STRINGjava.lang.String
INT_16[-2127, 2127-1] as a byte array whose + * length must be 16
U_INT_8[0, 264-1] as a byte array whose length must be 8
U_INT_16[0, 2128-1] as a byte array whose length must be 16
All other pairings are illegal and would cause an + * IllegalArgumentException exception. + * + * @see #URL + * @see #UUID + * @see #STRING + * @see #U_INT_8 + * @see #INT_16 + * @see #U_INT_16 + * + * @param valueType + * the data type of the object that is being created; must be one + * of the following: URL, UUID, + * STRING, INT_16, + * U_INT_8, or U_INT_16 + * + * @param value + * the value for the DataElement being created of + * type valueType + * + * @exception IllegalArgumentException + * if the value is not of the + * valueType type or is not in the range + * specified or is null + * + */ + + public DataElement(int valueType, Object value) + { + if (value == null) { + throw new IllegalArgumentException("value param is null"); + } + switch (valueType) { + case URL: + case STRING: + if (!(value instanceof String)) { + throw new IllegalArgumentException( + "value param should be String"); + } + break; + case UUID: + if (!(value instanceof java.util.UUID)) { + throw new IllegalArgumentException( + "value param should be UUID"); + } + break; + case U_INT_8: + if (!(value instanceof byte[]) || ((byte[])value).length != 8) { + throw new IllegalArgumentException( + "value param should be byte[8]"); + } + break; + case U_INT_16: + case INT_16: + if (!(value instanceof byte[]) || ((byte[])value).length != 16) { + throw new IllegalArgumentException( + "value param should be byte[16]"); + } + break; + case PRIVATEKEY: + // if (!(value instanceof byte[])) { + if (!(value instanceof BigInteger)) { + throw new IllegalArgumentException( + "value param should be BigInteger"); + } + byte[] tmpbuf = ((BigInteger)(value)).toByteArray(); + value = Arrays.copyOfRange(tmpbuf, 1, tmpbuf.length); + break; + case PUBLICKEY: + // if (!(value instanceof byte[])) { + if (!(value instanceof BigInteger)) { + throw new IllegalArgumentException( + "value param should be BigInteger"); + } + byte[] buf = ((BigInteger)(value)).toByteArray(); + byte[] buf2 = new byte[buf.length + 1]; + buf2[0] = 0x04; + System.arraycopy(buf, 0, buf2, 1, buf2.length - 1); + value = buf2; + break; + case SIGNATURE_D: + if (!(value instanceof byte[])) { + throw new IllegalArgumentException( + "value param should be byte[8]"); + } + break; + case SIGNATURE_H: + if (!(value instanceof byte[])) { + throw new IllegalArgumentException( + "value param should be byte[8]"); + } + break; + default: + throw new IllegalArgumentException( + "type " + typeToString(valueType) + + " can't be represented by Object"); + } + this.value = value; + this.valueType = valueType; + } + + /** + * Adds a DataElement to this DATALT or + * DATSEQ DataElement object. The + * elem will be added at the end of the list. The + * elem can be of any DataElement type, i.e., + * URL, NULL, BOOL, + * UUID, STRING, DATSEQ, + * DATALT, and the various signed and unsigned integer + * types. The same object may be added twice. If the object is successfully + * added the size of the DataElement is increased by one. + * + * @param elem + * the DataElement object to add + * + * @exception ClassCastException + * if the method is invoked on a DataElement + * whose type is not DATALT or + * DATSEQ + * + * @exception NullPointerException + * if elem is null + * + */ + + public void addElement(DataElement elem) + { + if (elem == null) { + throw new NullPointerException("elem param is null"); + } + switch (valueType) { + case DATALT: + case DATSEQ: + ((Vector)value).addElement(elem); + break; + default: + throw new ClassCastException("DataType is not DATSEQ or DATALT"); + } + } + + /** + * Inserts a DataElement at the specified location. This + * method can be invoked only on a DATALT or + * DATSEQ DataElement. elem + * can be of any DataElement type, i.e., URL, + * NULL, BOOL, UUID, + * STRING, DATSEQ, DATALT, + * and the various signed and unsigned integers. The same object may be + * added twice. If the object is successfully added the size will be + * increased by one. Each element with an index greater than or equal to the + * specified index is shifted upward to have an index one greater than the + * value it had previously. + *

+ * The index must be greater than or equal to 0 and less than + * or equal to the current size. Therefore, DATALT and + * DATSEQ are zero-based objects. + * + * @param elem + * the DataElement object to add + * + * @param index + * the location at which to add the DataElement + * + * @throws ClassCastException + * if the method is invoked on an instance of + * DataElement whose type is not + * DATALT or DATSEQ + * + * @throws IndexOutOfBoundsException + * if index is negative or greater than the size + * of the DATALT or DATSEQ + * + * @throws NullPointerException + * if elem is null + * + */ + + public void insertElementAt(DataElement elem, int index) + { + if (elem == null) { + throw new NullPointerException("elem param is null"); + } + switch (valueType) { + case DATALT: + case DATSEQ: + ((Vector)value).insertElementAt(elem, (short)index); + break; + default: + throw new ClassCastException("DataType is not DATSEQ or DATALT"); + } + } + + /** + * Returns the number of DataElements that are present in + * this DATALT or DATSEQ object. It is + * possible that the number of elements is equal to zero. + * + * @return the number of elements in this DATALT or + * DATSEQ + * + * @throws ClassCastException + * if this object is not of type DATALT or + * DATSEQ + */ + + public int getSize() + { + switch (valueType) { + case DATALT: + case DATSEQ: + return ((Vector)value).size(); + default: + throw new ClassCastException("DataType is not DATSEQ or DATALT"); + } + } + + /** + * Removes the first occurrence of the DataElement from this + * object. elem may be of any type, i.e., URL, + * NULL, BOOL, UUID, + * STRING, DATSEQ, DATALT, + * or the variously sized signed and unsigned integers. Only the first + * object in the list that is equal to elem will be removed. + * Other objects, if present, are not removed. Since this class doesn't + * override the equals() method of the Object + * class, the remove method compares only the references of objects. If + * elem is successfully removed the size of this + * DataElement is decreased by one. Each + * DataElement in the DATALT or + * DATSEQ with an index greater than the index of + * elem is shifted downward to have an index one smaller than + * the value it had previously. + * + * @param elem + * the DataElement to be removed + * + * @return true if the input value was found and removed; + * else false + * + * @throws ClassCastException + * if this object is not of type DATALT or + * DATSEQ + * + * @throws NullPointerException + * if elem is null + */ + + public boolean removeElement(DataElement elem) + { + if (elem == null) { + throw new NullPointerException("elem param is null"); + } + switch (valueType) { + case DATALT: + case DATSEQ: + return ((Vector)value).removeElement(elem); + default: + throw new ClassCastException("DataType is not DATSEQ or DATALT"); + } + } + + /** + * Returns the data type of the object this DataElement + * represents. + * + * @return the data type of this DataElement object; the legal + * return values are: + * URL, + * NULL, + * BOOL, + * UUID, + * STRING, + * DATSEQ, + * DATALT, + * U_INT_1, + * U_INT_2, + * U_INT_4, + * U_INT_8, + * U_INT_16, + * INT_1, + * INT_2, + * INT_4, + * INT_8, or + * INT_16 + * + */ + + public int getDataType() + { + return valueType; + } + + /** + * Returns the value of the DataElement if it can be + * represented as a long. The data type of the object must + * be U_INT_1, U_INT_2, U_INT_4, + * INT_1, INT_2, INT_4, or + * INT_8. + * + * + * @return the value of the DataElement as a + * long + * + * @throws ClassCastException + * if the data type of the object is not U_INT_1, + * U_INT_2, U_INT_4, + * INT_1, INT_2, + * INT_4, or INT_8 + */ + + public long getLong() + { + switch (valueType) { + case U_INT_1: + case U_INT_2: + case U_INT_4: + case INT_1: + case INT_2: + case INT_4: + case INT_8: + return ((Long)value).longValue(); + default: + throw new ClassCastException("DataType is not INT"); + } + } + + /** + * Returns the value of the DataElement if it is represented + * as a boolean. + * + * + * @return the boolean value of this DataElement + * object + * + * @throws ClassCastException + * if the data type of this object is not of type + * BOOL + */ + + public boolean getBoolean() + { + if (valueType == BOOL) { + return ((Boolean)value).booleanValue(); + } else { + throw new ClassCastException("DataType is not BOOL"); + } + } + + /** + * Returns the value of this DataElement as an + * Object. This method returns the appropriate Java object + * for the following data types: URL, UUID, + * STRING, DATSEQ, DATALT, + * U_INT_8, U_INT_16, and + * INT_16. Modifying the returned Object will + * not change this DataElement. + * + * The following are the legal pairs of data type and Java object type being + * returned. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
DataElement Data TypeJava Data Type
URLjava.lang.String
UUIDjavax.bluetooth.UUID
STRINGjava.lang.String
DATSEQjava.util.Enumeration
DATALTjava.util.Enumeration
U_INT_8byte[] of length 8
U_INT_16byte[] of length 16
INT_16byte[] of length 16
+ * + * @return the value of this object + * + * @throws ClassCastException + * if the object is not a URL, UUID, + * STRING, DATSEQ, + * DATALT, U_INT_8, U_INT_16, or + * INT_16 + * + */ + + public Object getValue() + { + switch (valueType) { + case URL: + case STRING: + case UUID: + return value; + case U_INT_8: + case U_INT_16: + case INT_16: + case PRIVATEKEY: + case PUBLICKEY: + case SIGNATURE_D: + case SIGNATURE_H: + // Modifying the returned Object will not change this DataElemen + return Helper.clone((byte[])value); + case DATSEQ: + case DATALT: + return ((Vector)value).elements(); + default: + throw new ClassCastException("DataType is simple java type"); + } + } + + private static String typeToString(int type) + { + switch (type) { + case DataElement.NULL: + return "NULL"; + case DataElement.U_INT_1: + return "U_INT_1"; + case DataElement.U_INT_2: + return "U_INT_2"; + case DataElement.U_INT_4: + return "U_INT_4"; + case DataElement.U_INT_8: + return "U_INT_8"; + case DataElement.U_INT_16: + return "U_INT_16"; + case DataElement.INT_1: + return "INT_1"; + case DataElement.INT_2: + return "INT_2"; + case DataElement.INT_4: + return "INT_4"; + case DataElement.INT_8: + return "INT_8"; + case DataElement.INT_16: + return "INT_16"; + case DataElement.URL: + return "URL"; + case DataElement.STRING: + return "STRING"; + case DataElement.UUID: + return "UUID"; + case DataElement.DATSEQ: + return "DATSEQ"; + case DataElement.BOOL: + return "BOOL"; + case DataElement.DATALT: + return "DATALT"; + case DataElement.PRIVATEKEY: + return "PRIVATEKEY"; + case DataElement.PUBLICKEY: + return "PUBLICKEY"; + case DataElement.SIGNATURE_D: + return "SIGNATURE_D"; + case DataElement.SIGNATURE_H: + return "SIGNATURE_H"; + default: + return "Unknown" + type; + } + } + + /** + * Non JSR-82 function. + * + * @deprecated Use ((Object)dataElement).toString() if you want your + * application to run in MDIP profile + */ + public String toString() + { + switch (valueType) { + case U_INT_1: + case U_INT_2: + case U_INT_4: + case INT_1: + case INT_2: + case INT_4: + case INT_8: + return typeToString(valueType) + " 0x" + + Helper.toHexString(((Long)value).longValue()); + case BOOL: + case URL: + case STRING: + case UUID: + case PRIVATEKEY: + case PUBLICKEY: + case SIGNATURE_D: + case SIGNATURE_H: + return typeToString(valueType) + " " + value.toString(); + case U_INT_8: + case U_INT_16: + case INT_16: { + byte[] b = (byte[])value; + + StringBuffer buf = new StringBuffer(); + buf.append(typeToString(valueType)).append(" "); + + for (int i = 0; i < b.length; i++) { + buf.append(Integer.toHexString(b[i] >> 4 & 0xf)); + buf.append(Integer.toHexString(b[i] & 0xf)); + } + + return buf.toString(); + } + case DATSEQ: { + StringBuffer buf = new StringBuffer("DATSEQ {\n"); + + for (Enumeration e = ((Vector)value).elements(); + e.hasMoreElements();) { + buf.append(e.nextElement()); + buf.append("\n"); + } + + buf.append("}"); + + return buf.toString(); + } + case DATALT: { + StringBuffer buf = new StringBuffer("DATALT {\n"); + + for (Enumeration e = ((Vector)value).elements(); + e.hasMoreElements();) { + buf.append(e.nextElement()); + buf.append("\n"); + } + + buf.append("}"); + + return buf.toString(); + } + default: + return "Unknown" + valueType; + } + } + + public boolean load(DataElement e) + { + this.value = e.value; + this.valueType = e.valueType; + return true; + } + + public boolean load(byte[] data) + { + boolean flag = false; + DataElement element = null; + in = new ByteArrayInputStream(data); + try { + element = readElement(); + this.value = element.value; + this.valueType = element.valueType; + flag = true; + } catch (IOException e) { + e.printStackTrace(); + } + return flag; + } + + public byte[] toByteArray() + { + if (out == null) { + out = new ByteArrayOutputStream(); + } else { + out.reset(); + } + try { + writeElement(this); + } catch (IOException e) { + e.printStackTrace(); + return new byte[0]; + } + + return this.out.toByteArray(); + } + + public DataElement(byte[] data) + { + load(data); + } + + public DataElement readElement() throws IOException + { + int header = read(); + int type = header >> 3 & 0x1f; + int sizeDescriptor = header & 0x07; + + pos++; + + switch (type) { + case 0: // NULL + return new DataElement(DataElement.NULL); + case 1: // U_INT + switch (sizeDescriptor) { + case 0: + return new DataElement(DataElement.U_INT_1, readLong(1)); + case 1: + return new DataElement(DataElement.U_INT_2, readLong(2)); + case 2: + return new DataElement(DataElement.U_INT_4, readLong(4)); + case 3: + return new DataElement(DataElement.U_INT_8, readBytes(8)); + case 4: + return new DataElement(DataElement.U_INT_16, readBytes(16)); + default: + throw new IOException(); + } + case 2: // INT + switch (sizeDescriptor) { + case 0: + return new DataElement(DataElement.INT_1, + (long)(byte)readLong(1)); + case 1: + return new DataElement(DataElement.INT_2, + (long)(short)readLong(2)); + case 2: + return new DataElement(DataElement.INT_4, + (long)(int)readLong(4)); + case 3: + return new DataElement(DataElement.INT_8, readLong(8)); + case 4: + return new DataElement(DataElement.INT_16, readBytes(16)); + default: + throw new IOException(); + } + case 3: // UUID + { + java.util.UUID uuid = null; + + switch (sizeDescriptor) { + case 1: + long msb = readLong(2); + uuid = new java.util.UUID(msb, 0); + break; + case 2: + uuid = new java.util.UUID(readLong(4), 0); + break; + case 4: + // uuid = new UUID(hexString(readBytes(16)), false); + uuid = java.util.UUID.nameUUIDFromBytes(readBytes(16)); + break; + default: + throw new IOException(); + } + + return new DataElement(DataElement.UUID, uuid); + } + case 4: // STRING + { + int length = -1; + + switch (sizeDescriptor) { + case 5: + length = readInteger(1); + break; + case 6: + length = readInteger(2); + break; + case 7: + length = readInteger(4); + break; + default: + throw new IOException(); + } + String strValue = Helper.newStringUTF8(readBytes(length)); + // DebugLog.debug("DataElement.STRING", strValue, + // Integer.toString(length - strValue.length())); + return new DataElement(DataElement.STRING, strValue); + } + case 5: // BOOL + return new DataElement(readLong(1) != 0); + case 6: // DATSEQ + { + int length; + + switch (sizeDescriptor) { + case 5: + length = readInteger(1); + break; + case 6: + length = readInteger(2); + break; + case 7: + length = readInteger(4); + break; + default: + throw new IOException(); + } + + DataElement element = new DataElement(DataElement.DATSEQ); + + int started = pos; + + for (int end = pos + length; pos < end;) { + element.addElement(readElement()); + } + if (started + length != pos) { + throw new IOException("DATSEQ size corruption " + + (started + length - pos)); + } + return element; + } + case 7: // DATALT + { + int length; + + switch (sizeDescriptor) { + case 5: + length = readInteger(1); + break; + case 6: + length = readInteger(2); + break; + case 7: + length = readInteger(4); + break; + default: + throw new IOException(); + } + + DataElement element = new DataElement(DataElement.DATALT); + + int started = pos; + + for (long end = pos + length; pos < end;) { + element.addElement(readElement()); + } + if (started + length != pos) { + throw new IOException("DATALT size corruption " + + (started + length - pos)); + } + return element; + } + case 8: // URL + { + int length; + + switch (sizeDescriptor) { + case 5: + length = readInteger(1); + break; + case 6: + length = readInteger(2); + break; + case 7: + length = readInteger(4); + break; + default: + throw new IOException(); + } + + return new DataElement(DataElement.URL, + Helper.newStringASCII(readBytes(length))); + } + case 9: // PRIVATEKEY + { + int length = readInteger(1); + byte[] byteseq = readBytes(length); + return new DataElement(DataElement.PRIVATEKEY, byteseq); + } + case 10: // PUBLICKEY + { + int length = readInteger(1); + byte[] byteseq = readBytes(length); + return new DataElement(DataElement.PUBLICKEY, byteseq); + } + case 11: // SIGNATURE_D + { + int length = readInteger(1); + byte[] byteseq = readBytes(length); + return new DataElement(DataElement.SIGNATURE_D, byteseq); + } + case 12: // SIGNATURE_H + { + int length = readInteger(1); + byte[] byteseq = readBytes(length); + return new DataElement(DataElement.SIGNATURE_H, byteseq); + } + default: + throw new IOException("Unknown type " + type); + } + } + + public int read() throws IOException + { + int v = in.read(); + return v; + } + + private long readLong(int size) throws IOException + { + long result = 0; + for (int i = 0; i < size; i++) { + result = result << 8 | read(); + } + pos += size; + return result; + } + + private int readInteger(int size) throws IOException + { + int result = 0; + for (int i = 0; i < size; i++) { + result = result << 8 | read(); + } + pos += size; + return result; + } + + private byte[] readBytes(int size) throws IOException + { + byte[] result = new byte[size]; + for (int i = 0; i < size; i++) { + result[i] = (byte)read(); + } + pos += size; + return result; + } + + private void writeElement(DataElement d) throws IOException + { + switch (d.getDataType()) { + case DataElement.NULL: + write(0 | 0); + break; + + case DataElement.U_INT_1: + write(8 | 0); + writeLong(d.getLong(), 1); + break; + case DataElement.U_INT_2: + write(8 | 1); + writeLong(d.getLong(), 2); + break; + case DataElement.U_INT_4: + write(8 | 2); + writeLong(d.getLong(), 4); + break; + case DataElement.U_INT_8: + write(8 | 3); + writeBytes((byte[])d.getValue()); + break; + case DataElement.U_INT_16: + write(8 | 4); + writeBytes((byte[])d.getValue()); + break; + + case DataElement.INT_1: + write(16 | 0); + writeLong(d.getLong(), 1); + break; + case DataElement.INT_2: + write(16 | 1); + writeLong(d.getLong(), 2); + break; + case DataElement.INT_4: + write(16 | 2); + writeLong(d.getLong(), 4); + break; + case DataElement.INT_8: + write(16 | 3); + writeLong(d.getLong(), 8); + break; + case DataElement.INT_16: + write(16 | 4); + writeBytes((byte[])d.getValue()); + break; + + case DataElement.UUID: + long uuid = Helper.UUIDTo32Bit((java.util.UUID)d.getValue()); + if (uuid == -1) { + write(24 | 4); + writeBytes( + Helper.UUIDToByteArray((java.util.UUID)d.getValue())); + } else if (uuid <= 0xFFFF) { + write(24 | 1); + writeLong(uuid, 2); + } else { + write(24 | 2); + writeLong(uuid, 4); + } + break; + + case DataElement.STRING: { + byte[] b; + b = Helper.getASCIIBytes((String)d.getValue()); + + if (b.length < 0x100) { + write(32 | 5); + writeLong(b.length, 1); + } else if (b.length < 0x10000) { + write(32 | 6); + writeLong(b.length, 2); + } else { + write(32 | 7); + writeLong(b.length, 4); + } + + writeBytes(b); + break; + } + + case DataElement.PRIVATEKEY: { + byte[] b = (byte[])d.getValue(); + write(72 | 5); + writeLong(b.length, 1); + writeBytes(b); + break; + } + + case DataElement.PUBLICKEY: { + byte[] b = (byte[])d.getValue(); + write(80 | 5); + writeLong(b.length, 1); + writeBytes(b); + break; + } + + case DataElement.SIGNATURE_D: { + byte[] b = (byte[])d.getValue(); + write(88 | 5); + writeLong(b.length, 1); + writeBytes(b); + break; + } + + case DataElement.SIGNATURE_H: { + byte[] b = (byte[])d.getValue(); + write(96 | 5); + writeLong(b.length, 1); + writeBytes(b); + break; + } + + case DataElement.BOOL: + write(40 | 0); + writeLong(d.getBoolean() ? 1 : 0, 1); + break; + + case DataElement.DATSEQ: { + int sizeDescriptor; + int len = getLength(d); + int lenSize; + if (len < (0xff + 2)) { + sizeDescriptor = 5; + lenSize = 1; + } else if (len < (0xFFFF + 3)) { + sizeDescriptor = 6; + lenSize = 2; + } else { + sizeDescriptor = 7; + lenSize = 4; + } + len -= (1 + lenSize); + write(48 | sizeDescriptor); + writeLong(len, lenSize); + + for (Enumeration e = (Enumeration)d.getValue(); + e.hasMoreElements();) { + writeElement((DataElement)e.nextElement()); + } + + break; + } + case DataElement.DATALT: { + int sizeDescriptor; + int len = getLength(d) - 5; + int lenSize; + if (len < 0xff) { + sizeDescriptor = 5; + lenSize = 1; + } else if (len < 0xFFFF) { + sizeDescriptor = 6; + lenSize = 2; + } else { + sizeDescriptor = 7; + lenSize = 4; + } + write(56 | sizeDescriptor); + writeLong(len, lenSize); + + for (Enumeration e = (Enumeration)d.getValue(); + e.hasMoreElements();) { + writeElement((DataElement)e.nextElement()); + } + + break; + } + case DataElement.URL: { + byte[] b; + + b = Helper.getASCIIBytes((String)d.getValue()); + + if (b.length < 0x100) { + write(64 | 5); + writeLong(b.length, 1); + } else if (b.length < 0x10000) { + write(64 | 6); + writeLong(b.length, 2); + } else { + write(64 | 7); + writeLong(b.length, 4); + } + + writeBytes(b); + break; + } + + default: + throw new IOException(); + } + } + + private void writeBytes(byte[] b) throws IOException + { + for (int i = 0; i < b.length; i++) { + write(b[i]); + } + } + + public void write(int oneByte) throws IOException + { + this.out.write(oneByte); + } + + private void writeLong(long l, int size) throws IOException + { + for (int i = 0; i < size; i++) { + write((int)(l >> (size - 1 << 3))); + l <<= 8; + } + } + + static int getLength(DataElement d) + { + switch (d.getDataType()) { + case DataElement.NULL: + return 1; + + case DataElement.BOOL: + case DataElement.U_INT_1: + case DataElement.INT_1: + return 2; + + case DataElement.U_INT_2: + case DataElement.INT_2: + return 3; + + case DataElement.U_INT_4: + case DataElement.INT_4: + return 5; + + case DataElement.U_INT_8: + case DataElement.INT_8: + return 9; + + case DataElement.U_INT_16: + case DataElement.INT_16: + return 17; + + case DataElement.UUID: + long uuid = Helper.UUIDTo32Bit((java.util.UUID)d.getValue()); + if (uuid == -1) { + return 1 + 16; + } else if (uuid <= 0xFFFF) { + return 1 + 2; + } else { + return 1 + 4; + } + case DataElement.STRING: { + byte[] b; + b = Helper.getASCIIBytes((String)d.getValue()); + if (b.length < 0x100) { + return b.length + 2; + } else if (b.length < 0x10000) { + return b.length + 3; + } else { + return b.length + 5; + } + } + case DataElement.PRIVATEKEY: { + byte[] b = (byte[])d.getValue(); + return b.length + 2; + } + case DataElement.PUBLICKEY: { + byte[] b = (byte[])d.getValue(); + return b.length + 2; + } + case DataElement.SIGNATURE_D: { + byte[] b = (byte[])d.getValue(); + return b.length + 2; + } + case DataElement.SIGNATURE_H: { + byte[] b = (byte[])d.getValue(); + return b.length + 2; + } + case DataElement.URL: { + byte[] b; + b = ((String)d.getValue()).getBytes(); + + if (b.length < 0x100) { + return b.length + 2; + } else if (b.length < 0x10000) { + return b.length + 3; + } else { + return b.length + 5; + } + } + + case DataElement.DATSEQ: + case DataElement.DATALT: { + int result = 1; + + for (Enumeration e = (Enumeration)d.getValue(); + e.hasMoreElements();) { + result += getLength((DataElement)e.nextElement()); + } + if (result < 0xff) { + result += 1; + } else if (result < 0xFFFF) { + result += 2; + } else { + result += 4; + } + + return result; + } + + default: + throw new IllegalArgumentException(); + } + } + + public static boolean validHeader(byte h) + { + for (short i = 0; i < validHeaders.length; i++) { + if (validHeaders[i] == h) { + return true; + } + } + + return false; + } + + public static byte[] extract(byte[] deBuf, byte t) + { + byte[] result = {}; + int n; + + for (int i = 0; i < deBuf.length;) { + byte header = deBuf[i]; + + if (!DataElement.validHeader(header)) { + return result; + } + + byte typeDesc = (byte)(header >> 3); + byte sizeDesc = (byte)(header & 0x07); + + switch (typeDesc) { + case TYPEDESC_NULL: + i++; + break; + case TYPEDESC_INT_1: // or TYPEDESC_INT_2 + switch (sizeDesc) { + case 0: // 1 byte + i += 2; + break; + case 1: // 2 bytes + i += 3; + break; + } + break; + case TYPEDESC_DATASEQ: + case TYPEDESC_DATALT: + switch (sizeDesc) { + case 5: + i += 2; + break; + case 6: + i += 3; + break; + } + break; + case TYPEDESC_PRIVATEKEY: + case TYPEDESC_PUBLICKEY: + case TYPEDESC_SIGNATURE_H: + case TYPEDESC_SIGNATURE_D: + i++; + n = deBuf[i]; + i++; + if (t == typeDesc) { + result = new byte[n]; + for (short idx = 0; idx < result.length; idx++) { + result[idx] = deBuf[i]; + i++; + } + return result; + } else { + i += n; + } + break; + } + } + + return result; + } +} diff --git a/offcard/src/main/java/org/idpass/offcard/proto/OffCard.java b/offcard/src/main/java/org/idpass/offcard/proto/OffCard.java new file mode 100644 index 0000000..c4e3554 --- /dev/null +++ b/offcard/src/main/java/org/idpass/offcard/proto/OffCard.java @@ -0,0 +1,649 @@ +package org.idpass.offcard.proto; + +import java.io.ByteArrayOutputStream; + +import java.io.IOException; +import java.security.SecureRandom; + +import java.util.Arrays; + +import org.bouncycastle.util.encoders.Hex; + +import javax.smartcardio.CardChannel; +import javax.smartcardio.CardException; +import javax.smartcardio.CommandAPDU; +import javax.smartcardio.ResponseAPDU; + +import org.idpass.offcard.applet.DummyISDApplet; +import org.idpass.offcard.misc.Helper; +import org.idpass.offcard.misc.Helper.Mode; +import org.idpass.offcard.misc.IdpassConfig; +import org.idpass.offcard.misc.Invariant; +import org.idpass.offcard.misc.Dump; + +import com.licel.jcardsim.utils.AIDUtil; + +import javacard.framework.AID; +import javacard.framework.ISO7816; +import javacard.framework.ISOException; +import javacard.framework.SystemException; +import javacard.framework.Util; +// import javacardx.crypto.Cipher; // @watch@ + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +// clang-format off +public class OffCard +{ + // Keys inside off-card + private static SCP02Keys offcardKeys[] = { + new SCP02Keys("404142434445464748494a4b4c4d4e4F", // 1 + "404142434445464748494a4b4c4d4e4F", + "404142434445464748494a4b4c4d4e4F"), + + new SCP02Keys("DEC0DE0102030405060708090A0B0C0D", // 2 + "DEC0DE0102030405060708090A0B0C0D", + "DEC0DE0102030405060708090A0B0C0D"), + + new SCP02Keys("CAFEBABE0102030405060708090A0B0C", // 3 + "CAFEBABE0102030405060708090A0B0C", + "CAFEBABE0102030405060708090A0B0C"), + + new SCP02Keys("C0FFEE0102030405060708090A0B0C0D", // 4 + "C0FFEE0102030405060708090A0B0C0D", + "C0FFEE0102030405060708090A0B0C0D"), + }; + + private static OffCard instance; + // clang-format on + + public static void reInitialize() + { + instance = null; + SCP02.reInitialize(); + Helper.reInitialize(); + } + + public static OffCard getInstance() + { + if (instance == null) { + try { + return getInstance(Helper.getjcardsimChannel()); + } catch (CardException e) { + System.out.println(e.getCause()); + } + } + return instance; + } + + public static OffCard getInstance(CardChannel chan) + { + if (instance == null) { + // install & select DummyISDApplet + if (chan != null) { + instance = new OffCard(chan); + } + } + + return instance; + } + + private CardChannel channel; + private String currentSelected; + private Invariant Assert = new Invariant(); + private SCP02 scp02; + private Mode mode; + + public Mode getMode() + { + return mode; + } + + private OffCard(CardChannel channel) + { + this.channel = channel; + + // This is the off-card side of the secure channel + scp02 = new SCP02(offcardKeys, "offcard"); + + String s = channel.getClass().getCanonicalName(); + if (s.equals( + "com.licel.jcardsim.smartcardio.CardSimulator.CardChannelImpl")) { + mode = Mode.SIM; + Helper.simulator.resetRuntime(); + INSTALL(DummyISDApplet.class); + select(DummyISDApplet.class); + } else { + mode = Mode.PHY; + // TODO: Use paljak's way to discover the CM + // and select it without involving any AID values + select(DummyISDApplet.class); + } + } + + public byte[] SELECT_CM() + { + byte[] retval = select(DummyISDApplet.class); + return retval; + } + + public void ATR() + { + if (mode == Mode.SIM) { + // simulator.reset(); // DO NOT CALL THIS method! + // This resets security level of previously selected applet + select(DummyISDApplet.class); // invoke security reset + } else if (mode == Mode.PHY) { + // card.getATR(); // ? + select(DummyISDApplet.class); // invoke security reset + } + } + + public Object INSTALL(Class cls) + { + // Get applet parameters + IdpassConfig cfg = cls.getAnnotation(IdpassConfig.class); + String strId = cfg.instanceAID(); + byte[] installParams = cfg.installParams(); + byte[] privileges = cfg.privileges(); + + byte[] bArray = {}; + byte[] id_bytes = Hex.decode(strId); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + bos.write(id_bytes.length); + bos.write(id_bytes); + bos.write(privileges.length); + if (privileges.length > 0) { + bos.write(privileges); + } + bos.write(installParams.length); + if (installParams.length > 0) { + bos.write(installParams); + } + bArray = bos.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + } + + AID aid = AIDUtil.create(id_bytes); + + switch (mode) { + case PHY: { + Method initMethod; + + try { + initMethod = cls.getMethod( + "install", + new Class[] {byte[].class, short.class, byte.class}); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException( + "Class does not provide install method"); + } + + try { + initMethod.invoke(null, bArray, (short)0, (byte)bArray.length); + } catch (InvocationTargetException e) { + try { + ISOException isoException = (ISOException)e.getCause(); + throw isoException; + } catch (ClassCastException cce) { + throw new SystemException(SystemException.ILLEGAL_AID); + } + } catch (Exception e) { + throw new SystemException(SystemException.ILLEGAL_AID); + } + } break; + + case SIM: + Helper.simulator.installApplet( + aid, cls, bArray, (short)0, (byte)bArray.length); + break; + } + + Object inst = null; + + try { + Method getinstance = cls.getMethod("getInstance"); + try { + inst = getinstance.invoke(null); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + } catch (NoSuchMethodException | SecurityException e) { + e.printStackTrace(); + } + + return inst; + } + + public byte[] select(Class cls) + { + this.scp02.resetSecurity(); + + byte[] result = new byte[] {(byte)0x6A, (byte)0xA2}; + + IdpassConfig cfg = cls.getAnnotation(IdpassConfig.class); + + String strId = cfg.instanceAID(); + byte[] id_bytes = Hex.decode(strId); + currentSelected = cls.getCanonicalName(); + + if (mode == Mode.SIM) { + result = Helper.simulator.selectAppletWithResult( + AIDUtil.create(id_bytes)); // @diff1_@ + } else if (mode == Mode.PHY) { + if (currentSelected.equals( + "org.idpass.offcard.applet.DummyISDApplet")) { + // Physical cards can have different CM AID + byte[] aid1 = Hex.decode("A0000001510000"); + byte[] aid2 = Hex.decode("D1560001320D0101"); + + result = selectAppletWithResult(aid1); + ResponseAPDU r = new ResponseAPDU(result); + if (r.getSW() != 0x9000) { + result = selectAppletWithResult(aid2); // try this one + } + } else { + result = selectAppletWithResult(id_bytes); // @diff1@ + } + } + + ResponseAPDU response = new ResponseAPDU(result); + Assert.assertEquals(response.getSW(), 0x9000, "OffCard::select"); + + Dump.print(result, String.format("SELECT %s", currentSelected)); + + return result; + } + + public byte[] Transmit(String rawbytes) + { + byte[] cmd = Hex.decode(rawbytes); + CommandAPDU command = new CommandAPDU(cmd); + ResponseAPDU response = Transmit(command); + return response.getBytes(); + } + + public ResponseAPDU Transmit(CommandAPDU apdu) + { + boolean flag = false; + byte sl = scp02.getSecurityLevel(); + + ResponseAPDU response = new ResponseAPDU(Helper.SW6701); + + byte[] tx = apdu.getBytes(); + byte[] rx = {}; + + // System.out.println("LC = " + buf[ISO7816.OFFSET_LC]); //@bad1@ + + byte cla = (byte)apdu.getCLA(); + final byte ins = (byte)apdu.getINS(); + final byte p1 = (byte)apdu.getP1(); + final byte p2 = (byte)apdu.getP2(); + final byte[] data = apdu.getData(); + byte origCLA = cla; + + // byte[] finalData = data; + byte[] newData = data; + byte[] M = {}; + + int newLc = apdu.getNc(); + + ByteArrayOutputStream t = new ByteArrayOutputStream(); + int le = apdu.getNe(); + + try { + t.write(data); + } catch (IOException e1) { + e1.printStackTrace(); + } + + if ((sl & SCP02.AUTHENTICATED) != 0 + || (sl & SCP02.C_MAC) != 0 + || (sl & SCP02.C_DECRYPTION) != 0) { + cla = (byte)(cla | SCP02.MASK_SECURED); + t.reset(); + + try { + t.write(cla); + t.write(ins); + t.write(p1); + t.write(p2); + + if ((sl & SCP02.C_MAC) != 0) { + newLc = newLc + 8; + + t.write(newLc); + t.write(data); + + byte[] input = t.toByteArray(); + M = scp02.computeMac(input); + newData = Helper.arrayConcat(data, M); + t.reset(); + } + + if ((sl & SCP02.C_DECRYPTION) != 0 && data.length > 0) { + byte[] dataPadded = CryptoAPI.pad80( + data, 8); // still needed due to len calculation!? + t.write(dataPadded); + newLc += t.size() - data.length; + + newData = CryptoAPI.encryptData( + data, scp02.sessionENC); // don't pad twice + + flag = true; + t.reset(); + } + + t.write(cla); + t.write(ins); + t.write(p1); + t.write(p2); + + // TODO: clean-up logic later to improve + if (newLc > 0) { + t.write(newLc); + t.write(newData); + } + + if (flag == true) { + if (M.length > 0) { + t.write(M); + } + } + + if (le > 0) { + t.write(le); + } + + } catch (IOException e) { + e.printStackTrace(); + } + } + + byte[] wrapCmdData = t.toByteArray(); + CommandAPDU command = null; + + if (M.length > 0) { + command = new CommandAPDU(wrapCmdData); + } else { + command = new CommandAPDU(origCLA, ins, p1, p2, wrapCmdData); + } + + Dump.print(command.getBytes(), "COMMAND"); + + try { + response = channel.transmit(command); + rx = response.getBytes(); + } catch (CardException e) { + rx = response.getBytes(); + } + + byte[] tx2 = command.getBytes(); + + System.out.println( + "\n----------------------------------- OffCard::Transmit -----------------------------------------"); + System.out.println(currentSelected + ": [" + Helper.printsL(sl) + "]"); + System.out.println(String.format("=> %s", Helper.print(tx))); + + if (!Arrays.equals(tx, tx2)) { + System.out.println(String.format("+=> %s", Helper.print(tx2))); + } + + System.out.println(String.format("<= %s", Helper.print(rx))); + Helper.printsL(sl); + System.out.println( + "-----------------------------------------------------------------------------------------------"); + return response; + } + + public byte[] INITIALIZE_UPDATE() + { + byte kvno = 0x00; + return INITIALIZE_UPDATE(kvno); + } + + public byte[] INITIALIZE_UPDATE(byte kvno) + { + scp02.resetSecurity(); + + SecureRandom random = new SecureRandom(); + random.nextBytes(scp02.host_challenge); + byte p1 = kvno; + byte p2 = 0x00; // Must be always 0x00 GPCardSpec v2.3.1 E.5.1.4 + Dump.print(scp02.host_challenge, "host_challenge"); + + CommandAPDU command + = new CommandAPDU(0x80, 0x50, p1, p2, scp02.host_challenge); + + ResponseAPDU response = new ResponseAPDU(Helper.SW9000); + + response = Transmit(command); + + if (response.getSW() != 0x9000) { + Dump.print(response.getBytes(), "INITIALIZE_UPDATE FAILED"); + return response.getBytes(); + } + + byte[] cardresponse = response.getData(); + byte[] keyInfo = new byte[2]; + // receive card's key information + Util.arrayCopyNonAtomic( + cardresponse, (short)10, keyInfo, (short)0, (byte)2); + + // from keyInfo, get keyset# chosen by card + byte index = keyInfo[0]; + byte proto = keyInfo[1]; + + if (proto != 0x02) { + ISOException.throwIt((short)ISO7816.SW_CONDITIONS_NOT_SATISFIED); + } + + // Save card_challenge! + Util.arrayCopyNonAtomic(cardresponse, + (short)12, + this.scp02.card_challenge, + (short)0, + (byte)this.scp02.card_challenge.length); + + byte[] seq = new byte[2]; + seq[0] = scp02.card_challenge[0]; + seq[1] = scp02.card_challenge[1]; + + byte[] cryptogram = new byte[8]; + + // Read cryptogram calculated by card + Util.arrayCopyNonAtomic(cardresponse, + (short)20, + cryptogram, + (short)0, + (byte)cryptogram.length); + + byte[] hostcard_challenge + = Helper.arrayConcat(scp02.host_challenge, scp02.card_challenge); + + if (scp02.setKeyIndex(index, seq) == false) { + String info + = String.format("Command failed: No such key: %d/1", index); + System.out.println(info); + return cardresponse; + } + + byte[] hostcard_cryptogram = scp02.calcCryptogram(hostcard_challenge); + Dump.print(hostcard_challenge, "hostcard_challenge"); + Dump.print(hostcard_cryptogram, "hostcard_cryptogram"); + + if (Arrays.equals(cryptogram, hostcard_cryptogram)) { + this.scp02.bInitUpdated = true; + } else { + System.out.println("Error code: -5 (Authentication failed)"); + System.out.println("Wrong response APDU: " + + Helper.print(response.getBytes())); + System.out.println("Error message: Card cryptogram invalid"); + Dump.print(response.getBytes(), "INITIALIZE_UPDATE FAILED"); + } + + return response.getBytes(); + } + + public byte[] EXTERNAL_AUTHENTICATE(byte securityLevel) + { + byte[] cardresponse = {}; + + if (scp02.bInitUpdated == false) { + scp02.resetSecurity(); + System.out.println("Error code: -7 (Illegal state)"); + System.out.println( + "Command failed: No SCP protocol found, need to run init-update first"); + + // ISOException.throwIt((short)ISO7816.SW_CONDITIONS_NOT_SATISFIED); + cardresponse = new ResponseAPDU(Helper.SW6985).getBytes(); + return cardresponse; + } + + byte p1 = securityLevel; + byte p2 = 0x00; // Must be always 0x00 (GPCardspec v2.3.1 E.5.2.4) + + if ((securityLevel & SCP02.C_DECRYPTION) != 0) { // if ENC is set + p1 = (byte)(p1 | SCP02.C_MAC); // then set MAC + } + + byte[] cardhost_challenge + = Helper.arrayConcat(scp02.card_challenge, scp02.host_challenge); + + byte[] cardhost_cryptogram = scp02.calcCryptogram(cardhost_challenge); + Dump.print(cardhost_challenge, "cardhost_challenge"); + Dump.print(cardhost_cryptogram, "cardhost_cryptogram"); + byte[] data = cardhost_cryptogram; + + ByteArrayOutputStream macData = new ByteArrayOutputStream(); + macData.write(0x84); + macData.write(0x82); + macData.write(p1); + macData.write(p2); + macData.write(data.length + 8); + try { + macData.write(data); + } catch (IOException e1) { + e1.printStackTrace(); + } + + byte[] mac = scp02.computeMac(macData.toByteArray()); + byte[] newData = Helper.arrayConcat(data, mac); + + CommandAPDU command + = new CommandAPDU(0x84, 0x82, p1, p2, newData); // add needsLE logic + ResponseAPDU response; + + response = Transmit(command); + cardresponse = response.getBytes(); + + if (response.getSW() == 0x9000) { + scp02.securityLevel + = (byte)(scp02.securityLevel | p1 | SCP02.AUTHENTICATED); + scp02.bInitUpdated = false; + // scp02.icv = CryptoAPI.NullBytes8.clone(); + + } else { + Dump.print(cardresponse, "EXTERNAL_AUTHENTICATE FAILED"); + } + + return cardresponse; + } + + public byte[] selectAppletWithResult( + byte[] id_bytes) // throws SystemException + { + byte[] result = {(byte)0x6A, (byte)0xA2}; + ResponseAPDU response = null; + CommandAPDU command = new CommandAPDU(0x00, 0xA4, 0x04, 0x00, id_bytes); + response = Transmit(command); + result = response.getBytes(); + return result; + } + + public void close() + { + Invariant.check(); + } + + /* + // Try to find GlobalPlatform from a card + public byte[] discover() + { + byte[] ret = {}; + + // Try the default + final CommandAPDU command = new CommandAPDU( + ISO7816.CLA_ISO7816, + ISO7816.INS_SELECT, + 0x04, + 0x00, + 256); + + ResponseAPDU response = Transmit(command); + + // Unfused JCOP replies with 0x6A82 to everything + if (response.getSW() == 0x6A82) { + // If it has the identification AID, it probably is an unfused JCOP + byte[] identify_aid = Hex.decode("A000000167413000FF"); + + CommandAPDU identify = new CommandAPDU( + ISO7816.CLA_ISO7816, + ISO7816.INS_SELECT, + 0x04, + 0x00, + identify_aid, + 256); + + ResponseAPDU identify_resp = channel.transmit(identify); + byte[] identify_data = identify_resp.getData(); + // Check the fuse state + if (identify_data.length > 15) { + if (identify_data[14] == 0x00) { + //throw new GPException("Unfused JCOP detected"); + return ret; + } + } + } + + // SmartJac UICC + if (response.getSW() == 0x6A87) { + // Try the default + //return connect(channel, new AID(GPData.defaultISDBytes)); + return Hex.decode("A000000151000000"); + + } + + final BerTlvs tlvs; + try { + // Detect security domain based on default select + BerTlvParser parser = new BerTlvParser(); + tlvs = parser.parse(response.getData()); + } catch (ArrayIndexOutOfBoundsException | IllegalStateException e) { + // XXX: Exists a card, which returns plain AID as response + //logger.warn("Could not parse SELECT response: " + e.getMessage()); + //throw new GPDataException("Could not auto-detect ISD AID", + response.getData()); return ret; + } + + BerTlv fcitag = tlvs.find(new BerTag(0x6F)); + if (fcitag != null) { + BerTlv isdaid = fcitag.find(new BerTag(0x84)); + // XXX: exists a card that returns a zero length AID in template + if (isdaid != null && isdaid.getBytesValue().length > 0) { + return isdaid.getBytesValue(); + } + } + + return ret; + } + */ +} diff --git a/offcard/src/main/java/org/idpass/offcard/proto/SCP02.java b/offcard/src/main/java/org/idpass/offcard/proto/SCP02.java new file mode 100644 index 0000000..606e13a --- /dev/null +++ b/offcard/src/main/java/org/idpass/offcard/proto/SCP02.java @@ -0,0 +1,434 @@ +package org.idpass.offcard.proto; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.idpass.offcard.misc.Helper; +import org.idpass.offcard.misc.Invariant; +import org.idpass.offcard.misc.Dump; + +import javacard.framework.APDU; +import javacard.framework.ISO7816; +import javacard.framework.ISOException; +import javacard.framework.Util; + +import java.security.SecureRandom; +import java.util.Arrays; + +import org.bouncycastle.util.encoders.Hex; + +// clang-format off +// CLA Byte Coding (not Security Level) +// 8 7 6 5 4 3 2 1 +// 0 0 0 0 - - - - Command defined in ISO/IEC 7816 +// 1 0 0 0 - - - - GlobalPlatform command +// - 0 0 0 0 0 - - No secure messaging +// - 0 0 0 0 1 - - Secure messaging - GlobalPlatform proprietary +// - 0 0 0 1 0 - - Secure messaging - ISO/IEC 7816 standard, command header not processed (no C-MAC) +// - 0 0 0 1 1 - - Secure messaging - ISO/IEC 7816 standard, command header authenticated (C-MAC) +// - 0 0 0 - - x x Logical channel number + +public class SCP02 implements org.globalplatform.SecureChannel +{ + public static final byte[] nxpDefaultKey = Hex.decode("404142434445464748494a4b4c4d4e4F"); + public static final byte[] otherTestKey = Hex.decode("CAFEBABE11223344CAFEBABE11223344"); + + public static final byte SECURE_MESSAGING_GP = (byte)0b00000100; + public static final byte SECURE_MESSAGING_ISO = (byte)0b00001000; + public static final byte MASK_SECURED = (byte)0b00001100; + public static final byte MASK_GP = (byte)0b10000000; + + public static final byte ANY_AUTHENTICATED = (byte)0b01000000; + + private static Invariant Assert = new Invariant(); + + public static final byte INS_INITIALIZE_UPDATE = (byte)0x50; + public static final byte INS_BEGIN_RMAC_SESSION = (byte)0x7A; + public static final byte INS_END_RMAC_SESSION = (byte)0x78; + + // GlobalPlatform Card Specification 2.1.1 E.1.2 Entity Authentication + private static short sequenceCounter = (short)0xAAA0; + private static byte[] diversification_data = Hex.decode("0102030405060708090A"); + // clang-format on + + public static int count; + + public static void reInitialize() + { + count = 0; + } + + public byte[] icv; + public byte cla; + private String entity; + + private byte[] keySetting = { + (byte)0xFF, + (byte)0x02, // scp02 + }; + + public SCP02Keys userKeys[]; + + public byte[] sessionENC; + public byte[] sessionMAC; + public byte[] sessionDEK; + + public boolean bInitUpdated = false; + public byte securityLevel = 0x00; + + public byte[] card_challenge = new byte[8]; + public byte[] host_challenge = new byte[8]; + + public byte[] computeMac(byte[] input) + { + byte[] icv; + + if (Arrays.equals(this.icv, CryptoAPI.NullBytes8)) { + icv = this.icv; + } else { + icv = CryptoAPI.updateIV(this.icv, this.sessionMAC); + } + + Dump.print(icv, String.format("MAC IV %s", entity)); + byte[] mac = CryptoAPI.computeMAC(input, icv, sessionMAC); + this.icv = mac.clone(); + + Dump.printline(this.sessionMAC, "sMAC"); + Dump.printline(input, "input"); + Dump.printline(mac, "mac"); + + return mac; + } + + public byte[] calcCryptogram(byte[] input) + { + byte[] cgram = {}; + + if (input != null && input.length > 0 && sessionENC != null + && sessionENC.length > 0) { + cgram = CryptoAPI.calcCryptogram(input, sessionENC); + } + + if (cgram.length == 0) { + System.out.println("Error calcCryptogram"); + ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); + } + + return cgram; + } + + public boolean setKeyIndex(int index, byte[] seq) + { + byte[] kEnc = null; + byte[] kMac = null; + byte[] kDek = null; + + if (index == (byte)0xFF) { + kEnc = nxpDefaultKey; + kMac = nxpDefaultKey; + kDek = nxpDefaultKey; + + } else { + try { + kEnc = userKeys[index - 1].kEnc; + kMac = userKeys[index - 1].kMac; + kDek = userKeys[index - 1].kDek; + + } catch (java.lang.ArrayIndexOutOfBoundsException e) { + return false; + } + } + + sessionENC + = CryptoAPI.deriveSCP02SessionKey(kEnc, seq, CryptoAPI.constENC); + sessionMAC + = CryptoAPI.deriveSCP02SessionKey(kMac, seq, CryptoAPI.constMAC); + sessionDEK + = CryptoAPI.deriveSCP02SessionKey(kDek, seq, CryptoAPI.constDEK); + + System.out.println(String.format("%s chosen keys = %s / %s / %s", + entity, + Dump.printline(kEnc, null), + Dump.printline(kMac, null), + Dump.printline(kDek, null))); + System.out.println(String.format("%s session keys = %s / %s / %s", + entity, + Dump.printline(sessionENC, null), + Dump.printline(sessionMAC, null), + Dump.printline(sessionDEK, null))); + + return true; + } + + public SCP02(SCP02Keys[] keys, String entity) + { + this.icv = CryptoAPI.NullBytes8.clone(); + count++; + + // One for DummyISDApplet + // One common for every IDPass applets + Assert.assertTrue(count <= 2, "SCP02::constructor"); + this.userKeys = keys.clone(); + byte preferred = (byte)Helper.getRandomKvno(keys.length); + keySetting[0] = preferred; + this.entity = entity; // either card or offcard + } + + @Override public short processSecurity(APDU apdu) throws ISOException + { + // System.out.println(String.format("SCP02::processSecurity + // [0x%02X]",securityLevel)); + byte[] buffer = APDU.getCurrentAPDUBuffer(); + byte ins = buffer[ISO7816.OFFSET_INS]; + byte p1 = buffer[ISO7816.OFFSET_P1]; + short responseLength = 0; + + switch (ins) { + case INS_INITIALIZE_UPDATE: + byte reqkvno = p1; // Get requested keyset# + byte index = reqkvno; + + // Get host_challenge + Util.arrayCopyNonAtomic(buffer, + (short)ISO7816.OFFSET_CDATA, + host_challenge, + (short)0x00, + (byte)host_challenge.length); + + // Card Specification V2.3.1 | GPC_SPE_034 (Mar 2018) + // E.5.1.3 Reference Control Parameter P1 - Key Version Number + if (reqkvno == 0x00) { + index = keySetting[0]; + } + + SecureRandom random = new SecureRandom(); + byte[] cardrandom = new byte[6]; // card generates 6 random bytes + random.nextBytes(cardrandom); + byte[] seq = new byte[2]; + Util.setShort(seq, (short)0, sequenceCounter); + + card_challenge = Helper.arrayConcat(seq, cardrandom); + + byte[] hostcard_challenge + = Helper.arrayConcat(host_challenge, card_challenge); + + if (setKeyIndex(index, seq) == false) { + String info + = String.format("Command failed: No such key: %d/1", index); + System.out.println(info); + ISOException.throwIt((short)Helper.SW_KEY_NOT_FOUND); + } + + byte[] hostcard_cryptogram = calcCryptogram(hostcard_challenge); + Dump.print(hostcard_challenge, "hostcard_challenge"); + Dump.print(hostcard_cryptogram, "hostcard_cryptogram"); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + // Prepare card response to offcard + bos.write(diversification_data); + bos.write(index); + bos.write(keySetting[1]); + bos.write(card_challenge); + bos.write(hostcard_cryptogram); + } catch (IOException e) { + e.printStackTrace(); + } + + byte[] response = bos.toByteArray(); + + // Write response to buffer + responseLength = (short)response.length; + Util.arrayCopyNonAtomic(response, + (short)0, + buffer, + (short)ISO7816.OFFSET_CDATA, + responseLength); + resetSecurity(); // clear security + bInitUpdated = true; + break; + + case ISO7816.INS_EXTERNAL_AUTHENTICATE: + // 4 bytes command + 1 byte len + 8 bytes cgram = 13 + byte[] mdata = new byte[13]; + byte[] cryptogram = new byte[8]; + byte[] mac1 = new byte[8]; + + Util.arrayCopyNonAtomic( + buffer, (short)0, mdata, (short)0x00, (byte)mdata.length); + + Util.arrayCopyNonAtomic(buffer, + (short)ISO7816.OFFSET_CDATA, + cryptogram, + (short)0x00, + (byte)cryptogram.length); + + Util.arrayCopyNonAtomic(buffer, + (short)(ISO7816.OFFSET_CDATA + 8), + mac1, + (short)0x00, + (byte)mac1.length); + + byte[] mac2 = computeMac(mdata); + + boolean cryptogram_ok = false; + boolean mac_ok = false; + + byte[] cardhost_challenge + = Helper.arrayConcat(card_challenge, host_challenge); + + byte[] cardhost_cryptogram = calcCryptogram(cardhost_challenge); + Dump.print(cardhost_challenge, "cardhost_challenge"); + Dump.print(cardhost_cryptogram, "cardhost_cryptogram"); + + if (Arrays.equals(cryptogram, cardhost_cryptogram)) { + cryptogram_ok = true; + } + + if (Arrays.equals(mac1, mac2)) { + mac_ok = true; + } + + if (bInitUpdated == true && cryptogram_ok && mac_ok) { + securityLevel = (byte)(securityLevel | p1 | AUTHENTICATED); + bInitUpdated = false; + responseLength = 0; + sequenceCounter++; + break; + } else { + resetSecurity(); + + if (!bInitUpdated) { + // "Command failed: No previous initialize update" + ISOException.throwIt( + (short)ISO7816.SW_CONDITIONS_NOT_SATISFIED); + } + + if (!cryptogram_ok) { + // Table E-12 + ISOException.throwIt((short)Helper.SW_VERIFICATION_FAILED); + } + + if (!mac_ok) { + ISOException.throwIt( + (short)ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED); + } + } + } + + return responseLength; + } + + @Override public void resetSecurity() + { + bInitUpdated = false; + securityLevel = 0x00; + this.icv = CryptoAPI.NullBytes8.clone(); + } + + @Override + public short unwrap(byte[] buf, short arg1, short arg2) throws ISOException + { + short retval = arg2; + byte[] decrypted = {}; + + System.out.println("SCP02::unwrap"); + Dump.print(buf, arg2); + short len = (short)(buf[ISO7816.OFFSET_LC] & 0xFF); + byte[] cmd = new byte[len]; + Util.arrayCopyNonAtomic(buf, ISO7816.OFFSET_CDATA, cmd, (short)0, len); + + byte[] mactag = null; + int datalen = len; + + if ((securityLevel & C_MAC) != 0) { + mactag = new byte[8]; + // Get MAC tag first. + // Its verification is after decryption if encrypted + Util.arrayCopyNonAtomic(cmd, + (short)(cmd.length - 8), + mactag, + (short)0, + (short)mactag.length); + + datalen = datalen - 8; + } + + if (((securityLevel & C_DECRYPTION) != 0) && datalen > 0) { + byte[] encrypted = new byte[datalen]; + Util.arrayCopyNonAtomic(cmd, + (short)0, + encrypted, + (short)0, + (short)(datalen)); // don't copy mac tag + + Dump.print(encrypted, "encrypted"); + decrypted = CryptoAPI.decryptData(encrypted, sessionENC); + Dump.print(decrypted, "decrypted"); + + byte[] header = new byte[5]; + Util.arrayCopyNonAtomic( + buf, (short)0, header, (short)0, (short)header.length); + header[ISO7816.OFFSET_LC] = (byte)(8 + decrypted.length); + byte[] combined = Helper.arrayConcat(header, decrypted); + byte[] mComputed = computeMac(combined); + Assert.assertEquals(mComputed, mactag, "MAC Tag mismatch"); + + retval = Util.arrayCopyNonAtomic(decrypted, + (short)0, + buf, + (short)ISO7816.OFFSET_CDATA, + (short)decrypted.length); + } else { + byte[] headN = new byte[5]; // 4 bytes command apdu + 1 byte length + Util.arrayCopyNonAtomic( + buf, (short)0, headN, (short)0, (short)(headN.length)); + byte[] mComputed = computeMac(headN); + Dump.print(mComputed, "mComputed"); + Dump.print(mactag, "mactag"); + Assert.assertEquals(mComputed, mactag, "MAC tag mismatch"); + headN[ISO7816.OFFSET_LC] = 0; + + retval + = (short)(Util.arrayCopyNonAtomic(headN, + (short)0, + buf, + (short)ISO7816.OFFSET_CDATA, + (short)headN.length) + - headN.length); + } + + return retval; + } + + @Override + public short wrap(byte[] buf, short arg1, short arg2) + throws ArrayIndexOutOfBoundsException, ISOException + { + System.out.println("SCP02::wrap"); + return arg2; + } + + @Override + public short decryptData(byte[] buf, short arg1, short arg2) + throws ISOException + { + System.out.println("SCP02::decryptData"); + return 0; + } + + @Override + public short encryptData(byte[] buf, short arg1, short arg2) + throws ArrayIndexOutOfBoundsException + { + System.out.println("SCP02::encryptData"); + return 0; + } + + @Override public byte getSecurityLevel() + { + // System.out.println(String.format("SCP02::securityLevel = + // 0x%02X",securityLevel)); + return securityLevel; + } +} diff --git a/offcard/src/main/java/org/idpass/offcard/proto/SCP02Keys.java b/offcard/src/main/java/org/idpass/offcard/proto/SCP02Keys.java new file mode 100644 index 0000000..9fef699 --- /dev/null +++ b/offcard/src/main/java/org/idpass/offcard/proto/SCP02Keys.java @@ -0,0 +1,17 @@ +package org.idpass.offcard.proto; + +import com.licel.jcardsim.bouncycastle.util.encoders.Hex; + +public class SCP02Keys +{ + public byte[] kEnc; + public byte[] kMac; + public byte[] kDek; + + public SCP02Keys(String kEnc, String kMac, String kDek) + { + this.kEnc = Hex.decode(kEnc); + this.kMac = Hex.decode(kMac); + this.kDek = Hex.decode(kDek); + } +} diff --git a/offcard/src/test/java/org/idpass/offcard/test/Main.java b/offcard/src/test/java/org/idpass/offcard/test/Main.java new file mode 100644 index 0000000..b9f7632 --- /dev/null +++ b/offcard/src/test/java/org/idpass/offcard/test/Main.java @@ -0,0 +1,884 @@ +package org.idpass.offcard.test; + +import java.util.Arrays; +import java.util.Enumeration; +import java.util.UUID; +import java.lang.reflect.Method; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + +import org.idpass.offcard.proto.DataElement; +import org.idpass.offcard.proto.OffCard; +import org.idpass.offcard.proto.SCP02; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.idpass.offcard.applet.AuthApplet; +import org.idpass.offcard.applet.DatastorageApplet; +import org.idpass.offcard.applet.SamApplet; +import org.idpass.offcard.applet.SignApplet; +import org.idpass.offcard.applet.DecodeApplet; // tiny development applet for testing + +import org.testng.annotations.*; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.Hash; +import org.web3j.crypto.Sign; +import org.web3j.crypto.TransactionEncoder; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.DefaultBlockParameterName; +import org.web3j.protocol.core.methods.request.RawTransaction; +import org.web3j.protocol.core.methods.response.EthSendTransaction; +import org.web3j.protocol.http.HttpService; +import org.web3j.tx.Transfer; +import org.web3j.utils.Convert; +import org.web3j.utils.Numeric; +import org.idpass.offcard.misc.Invariant; +// import com.licel.jcardsim.bouncycastle.util.encoders.Hex; +import org.bouncycastle.util.encoders.Hex; +import java.security.Security; +import org.idpass.offcard.misc.Helper; +import javacard.framework.Util; +import javax.smartcardio.CardException; +import org.idpass.offcard.misc.Dump; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.math.BigInteger; + +public class Main +{ + static + { + Assert = new Invariant(true); // hard assert + } + private static Invariant Assert; + // TODO: Convert this long ascii string to pure byte array initialization + private static byte[] verifierTemplateData = Hex.decode( + "8200910210007F2E868184268B8129A7402DAC91335793342B8437814237C24238D34238E0423EEE423F4F43433F44521A45662D956D664470745379F2527DE64286EF42905B8697939297A0919AF3929F8D94A2878FA3948FA4A250AB854CB0C651B8CF41B8DA51CAA050D03C4CD54D5DD7175BDBBB50E0255CE5415DE72C4CE7FE41F1B05EF2914EF9C880FC258B"); + + private static byte[] candidate = Hex.decode( + "7F2E868184268B8129A7402DAC91335793342B8437814237C24238D34238E0423EEE423F4F43433F44521A45662D956D664470745379F2527DE64286EF42905B8697939297A0919AF3929F8D94A2878FA3948FA4A250AB854CB0C651B8CF41B8DA51CAA050D03C4CD54D5DD7175BDBBB50E0255CE5415DE72C4CE7FE41F1B05EF2914EF9C880FC258B"); + private static byte[] pin6 = Hex.decode("313233343536"); + private static byte[] pin7 = Hex.decode("31323334353637"); + + private final static short MINIMUM_SUCCESSFUL_MATCH_SCORE = 0x4000; + + static byte[] app01 = { + (byte)0xAA, + (byte)0xAA, + (byte)0xAA, + (byte)0x11, + (byte)0x11, + }; + static byte[] app02 = { + (byte)0xBB, + (byte)0xBB, + (byte)0xBB, + (byte)0x22, + (byte)0x22, + }; + static byte[] app03 = { + (byte)0xCC, + (byte)0xCC, + (byte)0xCC, + (byte)0x33, + (byte)0x33, + }; + + static byte[] app010203 = { + (byte)0xAA, + (byte)0xAA, + (byte)0xAA, + (byte)0xBB, + (byte)0xBB, + (byte)0xBB, + (byte)0xCC, + (byte)0xCC, + (byte)0xCC, + }; + + public static void main(String[] args) + { + try { + // test_DataElement_cardside(); + signTransactionTest(); + // test_SignApplet_physical(); + // test_SignApplet_virtual(); + // verifierTemplateTest_physical_card(); + // circleci_I_SUCCESS_TEST(); + // circleci_DATASTORAGE_TEST(); + // circleci_persona_add_delete(); + } catch (CardException e) { + } catch (IllegalStateException e) { + System.out.println("ERROR IllegalStateException: " + e.getCause()); + } catch (RuntimeException e) { + System.out.println("ERROR RunTimeException: " + e.getCause()); + } catch (Exception e) { + System.out.println("ERROR Exception: " + e.getCause()); + } + + Invariant.check(); + } + + @BeforeMethod public static void circleci_do_beforetest() + { + OffCard.reInitialize(); + } + + @Test public static void circleci_I_SUCCESS_TEST() throws CardException + { + System.out.println( + "#####################################################\n" + + "I_SUCCESS TEST START\n" + + "#####################################################\n"); + + short p; + + OffCard offcard = OffCard.getInstance(); + + DatastorageApplet datastorage + = (DatastorageApplet)offcard.INSTALL(DatastorageApplet.class); + SamApplet sam = (SamApplet)offcard.INSTALL(SamApplet.class); + AuthApplet auth = (AuthApplet)offcard.INSTALL(AuthApplet.class); + + // AuthApplet tests + auth.SELECT(); + offcard.INITIALIZE_UPDATE(); + offcard.EXTERNAL_AUTHENTICATE(SCP02.C_DECRYPTION); // 0x02 + + auth.processAddListener(datastorage.aid()); + auth.processAddListener(sam.aid()); + p = auth.processAddPersona(); //@ + auth.processAddVerifierForPersona( + (byte)p, pin6); // pin set at AuthApplet annotation + + offcard.ATR(); + + auth.SELECT(); + // offcard.INITIALIZE_UPDATE(); // channel not secured + auth.processAuthenticatePersona(pin6); //@ + + // SamApplet tests + String inData + = "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog"; + byte[] plainText = inData.getBytes(); + byte[] cipherText; + sam.SELECT(); + cipherText = sam.processEncrypt(plainText); + byte[] decrypted = sam.processDecrypt(cipherText); + + if (Arrays.equals(plainText, decrypted)) { + System.out.println("** match ***"); + } + + // Datastorage tests + datastorage.SELECT(); + p = datastorage.processSwitchNextVirtualCard(); + p = datastorage.processSwitchNextVirtualCard(); + p = datastorage.processSwitchNextVirtualCard(); + datastorage.SELECT(); + p = datastorage.processSwitchNextVirtualCard(); + + offcard.ATR(); + datastorage.SELECT(); + p = datastorage.processSwitchNextVirtualCard(); + + auth.SELECT(); + offcard.INITIALIZE_UPDATE(); + offcard.EXTERNAL_AUTHENTICATE(SCP02.C_MAC); // 0x01 + auth.processDeletePersona((byte)0x00); //@ + offcard.INITIALIZE_UPDATE(); + offcard.EXTERNAL_AUTHENTICATE(SCP02.C_DECRYPTION); // 0x02 + auth.processDeleteListener(datastorage.aid()); + auth.processDeleteListener(sam.aid()); + + System.out.println( + "#####################################################\n" + + "SUCCESS TEST DONE\n" + + "#####################################################\n"); + Invariant.check(); + } + + @Test public static void circleci_persona_add_delete() throws CardException + { + byte[] byteseq = {}; + + OffCard card = OffCard.getInstance(); + AuthApplet auth = (AuthApplet)card.INSTALL(AuthApplet.class); + + byteseq = auth.SELECT(); + Assert.assertTrue(Helper.checkstatus(byteseq)); + + short index = auth.processAddPersona(); + Assert.assertEquals(index, (short)0xFFFF); + + byteseq = card.INITIALIZE_UPDATE(); + Assert.assertTrue(byteseq.length == 30); + Assert.assertTrue(Helper.checkstatus(byteseq)); + + card.EXTERNAL_AUTHENTICATE(SCP02.C_MAC); + index = auth.processAddPersona(); + Assert.assertEquals(index, (short)0x0000); + + card.ATR(); + + index = auth.processAddPersona(); + Assert.assertEquals(index, (short)0xFFFF); + + card.ATR(); + + byteseq = card.EXTERNAL_AUTHENTICATE(SCP02.C_MAC); + Assert.assertFalse(Helper.checkstatus(byteseq)); + Assert.assertEquals(byteseq, Helper.SW6985); + + byteseq = card.INITIALIZE_UPDATE(); + Assert.assertTrue(Helper.checkstatus(byteseq)); + byteseq = card.EXTERNAL_AUTHENTICATE(SCP02.C_MAC); + Assert.assertTrue(Helper.checkstatus(byteseq)); + index = auth.processAddPersona(); + Assert.assertEquals(index, (short)0xFFFF); + + auth.SELECT(); + card.INITIALIZE_UPDATE(); + card.EXTERNAL_AUTHENTICATE(SCP02.C_MAC); + + index = auth.processAddPersona(); + Assert.assertEquals(index, (short)0x0001); + index = auth.processAddPersona(); + Assert.assertEquals(index, (short)0x0002); + + card.ATR(); + byteseq = auth.SELECT(); + Assert.assertTrue(Helper.checkstatus(byteseq)); + short count = Util.makeShort(byteseq[0], byteseq[1]); + Assert.assertEquals((short)3, count, "Added 3 Personas"); + + card.INITIALIZE_UPDATE(); + card.EXTERNAL_AUTHENTICATE(SCP02.C_MAC); + auth.processDeletePersona((byte)0); + + byteseq = auth.SELECT(); + count = Util.makeShort(byteseq[0], byteseq[1]); + + auth.processDeletePersona((byte)1); + byteseq = auth.SELECT(); + count = Util.makeShort(byteseq[0], byteseq[1]); + Assert.assertEquals((short)2, count, "Delete requires secure channel"); + + card.INITIALIZE_UPDATE(); + card.EXTERNAL_AUTHENTICATE(SCP02.C_MAC); + auth.processDeletePersona((byte)1); + auth.processDeletePersona((byte)2); + byteseq = auth.SELECT(); + count = Util.makeShort(byteseq[0], byteseq[1]); + Assert.assertEquals((short)0, count, "Deleted 3 Personas"); + } + + @Test public static void circleci_DATASTORAGE_TEST() throws CardException + { + System.out.println( + "#####################################################\n" + + "DATASTORAGE TEST START\n" + + "#####################################################\n"); + + OffCard offcard = OffCard.getInstance(); + + byte[] ret = null; + short p; + byte[] verifierTemplateData = new byte[10]; + + DatastorageApplet datastorage + = (DatastorageApplet)offcard.INSTALL(DatastorageApplet.class); + AuthApplet auth = (AuthApplet)offcard.INSTALL(AuthApplet.class); + + byte[] desfireCmd = { + (byte)0x90, + (byte)0x6A, + (byte)0x00, + (byte)0x00, + (byte)0x00, + }; + + auth.SELECT(); + offcard.INITIALIZE_UPDATE(); + offcard.EXTERNAL_AUTHENTICATE((byte)(SCP02.C_DECRYPTION | SCP02.C_MAC)); + + auth.processAddListener(datastorage.aid()); + p = auth.processAddPersona(); //@ + auth.processAddVerifierForPersona((byte)p, verifierTemplateData); + auth.processAuthenticatePersona(verifierTemplateData); //@ + + datastorage.SELECT(); + datastorage.processSwitchNextVirtualCard(); + datastorage.processSwitchNextVirtualCard(); + datastorage.processSwitchNextVirtualCard(); + + ret = datastorage.GET_APPLICATION_IDS(); + + datastorage.CREATE_APPLICATION(app01); + datastorage.CREATE_APPLICATION(app02); + datastorage.CREATE_APPLICATION(app03); + + ret = datastorage.GET_APPLICATION_IDS(); + + Assert.assertTrue(Arrays.equals(ret, app010203), + "three desfire applist"); + + // deleting in this order for now, pending investigation why deleting in + // the middle or in the front sparsely confuses datastorage resulting to + // holes + datastorage.DELETE_APPLICATION( + new byte[] {(byte)0xCC, (byte)0xCC, (byte)0xCC}); + ret = datastorage.GET_APPLICATION_IDS(); + Assert.assertTrue(Arrays.equals(ret, + new byte[] {(byte)0xAA, + (byte)0xAA, + (byte)0xAA, + (byte)0xBB, + (byte)0xBB, + (byte)0xBB}), + "desfire applist - 1"); + datastorage.DELETE_APPLICATION( + new byte[] {(byte)0xBB, (byte)0xBB, (byte)0xBB}); + ret = datastorage.GET_APPLICATION_IDS(); + Assert.assertTrue( + Arrays.equals(ret, new byte[] {(byte)0xAA, (byte)0xAA, (byte)0xAA}), + "desfire applist - 2"); + datastorage.DELETE_APPLICATION( + new byte[] {(byte)0xAA, (byte)0xAA, (byte)0xAA}); + ret = datastorage.GET_APPLICATION_IDS(); + Assert.assertTrue(ret == null, "desfire applist should be zero"); + + offcard.ATR(); + datastorage.SELECT(); + + System.out.println( + "#####################################################\n" + + "DATASTORAGE TEST END\n" + + "#####################################################\n"); + + Invariant.check(); + } + + // Assuming all applets installed up to: + // - processAddListener ${datastorageInstanceAID} + // - processAddPersona + // - processAddVerifierForPersona 00 ${verifierTemplateData} + // - processAuthenticatePersona + // + // This setups datastorage to switch propertly + // and at least 1 persona for testing + @Test public static void PHYSICAL_CARD_TEST() throws CardException + { + OffCard offcard = OffCard.getInstance(Helper.getPcscChannel()); + if (offcard == null) { + System.out.println( + "No physical reader/card found. Gracefully exiting."); + return; + } + + DatastorageApplet datastorage + = (DatastorageApplet)offcard.INSTALL(DatastorageApplet.class); + AuthApplet auth = (AuthApplet)offcard.INSTALL(AuthApplet.class); + + offcard.SELECT_CM(); + auth.SELECT(); + + // Check initial secure channel handshake + offcard.INITIALIZE_UPDATE(); + offcard.EXTERNAL_AUTHENTICATE( + (byte)(SCP02.C_DECRYPTION | SCP02.C_MAC)); // 0x03 + + // Temporarily clear secure channel, pending todo in IV chaining. + // The IV is not yet fully synchronized and subsequent secure messages + // past secure channel handshake fails to verify + offcard.INITIALIZE_UPDATE(); + + auth.processAuthenticatePersona(candidate); + datastorage.SELECT(); + + datastorage.processSwitchNextVirtualCard(); + datastorage.SELECT(); + datastorage.processSwitchNextVirtualCard(); + + Invariant.check(); + } + + @Test + public static void test_SignApplet_virtual() + throws CardException, UnsupportedEncodingException + { + byte[] data = "hello world test message".getBytes("UTF-8"); + byte[] ret = {}; + + OffCard card = OffCard.getInstance(); + SignApplet signer = (SignApplet)card.INSTALL(SignApplet.class); + AuthApplet auth = (AuthApplet)card.INSTALL(AuthApplet.class); + + auth.SELECT(); + card.INITIALIZE_UPDATE(); + card.EXTERNAL_AUTHENTICATE((byte)(SCP02.C_DECRYPTION | SCP02.C_MAC)); + auth.processAddListener(signer.aid()); + short index = auth.processAddPersona(); + auth.processAddVerifierForPersona((byte)index, pin6); + auth.processAuthenticatePersona(pin7); + + ret = signer.SELECT(); + Dump.print(ret, "SignApplet select retval"); + + ret = signer.sign(data, 0); + Dump.print(ret, "signature"); + Assert.assertTrue(ret.length == 0); + + auth.SELECT(); + // auth.processAddListener(signer.aid()); // sign applet must listen + card.INITIALIZE_UPDATE(); + card.EXTERNAL_AUTHENTICATE((byte)(SCP02.C_DECRYPTION | SCP02.C_MAC)); + auth.processAuthenticatePersona(pin6); + + signer.SELECT(); + ret = signer.sign(data, 0); + Dump.print(ret, "signature"); + Assert.assertTrue(ret.length > 0); + + Invariant.check(); + } + + @Test + public static void test_SignApplet_physical() + throws CardException, UnsupportedEncodingException + { + byte[] data = "hello world test message".getBytes("UTF-8"); + byte[] ret = {}; + + OffCard card = OffCard.getInstance(Helper.getPcscChannel()); + if (card == null) { + System.out.println( + "No physical reader/card found. Gracefully exiting."); + return; + } + SignApplet signer = (SignApplet)card.INSTALL(SignApplet.class); + AuthApplet auth = (AuthApplet)card.INSTALL(AuthApplet.class); + + auth.SELECT(); + card.INITIALIZE_UPDATE(); + card.EXTERNAL_AUTHENTICATE((byte)(SCP02.C_DECRYPTION | SCP02.C_MAC)); + auth.processAddListener(signer.aid()); + short index = auth.processAddPersona(); + auth.processAddVerifierForPersona((byte)index, verifierTemplateData); + auth.processAuthenticatePersona(pin7); + + ret = signer.SELECT(); + Dump.print(ret, "SignApplet select retval"); + + ret = signer.sign(data, 0); + Dump.print(ret, "signature"); + Assert.assertTrue(ret.length == 0); + + auth.SELECT(); + card.INITIALIZE_UPDATE(); + card.EXTERNAL_AUTHENTICATE((byte)(SCP02.C_DECRYPTION | SCP02.C_MAC)); + auth.processAuthenticatePersona(candidate); + + signer.SELECT(); + ret = signer.sign(data, 0); + Dump.print(ret, "signature"); + Assert.assertTrue(ret.length > 0); + + Invariant.check(); + } + + @Test + public static void verifierTemplateTest_physical_card() throws CardException + { + Security.addProvider(new BouncyCastleProvider()); + boolean physical = true; + + OffCard offcard = null; + + if (physical) { + offcard = OffCard.getInstance(Helper.getPcscChannel()); + } else { + offcard = OffCard.getInstance(); + } + + if (offcard == null) { + System.out.println( + "No physical reader/card found. Gracefully exiting."); + return; + } + + short n = (short)0xFFFF; + byte[] byteseq = {}; + + DatastorageApplet datastorage + = (DatastorageApplet)offcard.INSTALL(DatastorageApplet.class); + AuthApplet auth = (AuthApplet)offcard.INSTALL(AuthApplet.class); + + offcard.SELECT_CM(); + byteseq = auth.SELECT(); + Dump.print(byteseq); + + offcard.INITIALIZE_UPDATE(); + offcard.EXTERNAL_AUTHENTICATE( + (byte)(SCP02.C_DECRYPTION | SCP02.C_MAC)); // 0x03 + + auth.processAddListener(datastorage.aid()); + n = auth.processAddPersona(); + auth.processAddVerifierForPersona((byte)n, verifierTemplateData); + + if (physical) { + int score = auth.processAuthenticatePersona(candidate); + System.out.println(String.format("score = 0x%08X", score)); + Assert.assertEquals((short)(score & 0xFFFF), + MINIMUM_SUCCESSFUL_MATCH_SCORE, + "biometrics pass"); + } else { + int score = auth.processAuthenticatePersona(candidate); + System.out.println(String.format("score = 0x%08X", score)); + Assert.assertEquals( + (short)(score & 0xFFFF), 0x0000, "simple pin match"); + } + byteseq = datastorage.SELECT(); + Dump.print(byteseq); + n = datastorage.processSwitchNextVirtualCard(); + byteseq = datastorage.SELECT(); + Dump.print(byteseq); + n = datastorage.processSwitchNextVirtualCard(); + + Invariant.check(); + } + + // NOTE: verifier type setting is set in class annotation + @Test public static void pin6MatchTest() throws CardException + { + Security.addProvider(new BouncyCastleProvider()); + + OffCard offcard = OffCard.getInstance(); + + short n = (short)0xFFFF; + byte[] byteseq = {}; + + DatastorageApplet datastorage + = (DatastorageApplet)offcard.INSTALL(DatastorageApplet.class); + AuthApplet auth = (AuthApplet)offcard.INSTALL(AuthApplet.class); + + offcard.SELECT_CM(); + byteseq = auth.SELECT(); + Dump.print(byteseq); + + offcard.INITIALIZE_UPDATE(); + offcard.EXTERNAL_AUTHENTICATE( + (byte)(SCP02.C_DECRYPTION | SCP02.C_MAC)); // 0x03 + + auth.processAddListener(datastorage.aid()); + n = auth.processAddPersona(); + auth.processAddVerifierForPersona((byte)n, pin6); + + int score = auth.processAuthenticatePersona(pin6); + System.out.println(String.format("score = 0x%08X", score)); + Assert.assertEquals(score, 0x00000000, "pin match score 100%"); + byteseq = datastorage.SELECT(); + Dump.print(byteseq); + n = datastorage.processSwitchNextVirtualCard(); + byteseq = datastorage.SELECT(); + Dump.print(byteseq); + n = datastorage.processSwitchNextVirtualCard(); + + Invariant.check(); + } + + @Test public static void signTransactionTest() throws Exception + { + boolean physical = true; + OffCard card = null; + card = OffCard.getInstance(physical ? Helper.getPcscChannel() : + Helper.getjcardsimChannel()); + if (card == null) { + System.out.println("NO PCSC channel"); + return; + } + + SignApplet signer = (SignApplet)card.INSTALL(SignApplet.class); + AuthApplet auth = (AuthApplet)card.INSTALL(AuthApplet.class); + + auth.SELECT(); + card.INITIALIZE_UPDATE(); + card.EXTERNAL_AUTHENTICATE((byte)(SCP02.C_DECRYPTION | SCP02.C_MAC)); + + auth.processAddListener(signer.aid()); + short index = auth.processAddPersona(); + + if (physical) { + // This for physical card + auth.processAddVerifierForPersona((byte)index, + verifierTemplateData); + auth.processAuthenticatePersona(candidate); + } else { + // This for jcardsim + auth.processAddVerifierForPersona((byte)index, pin6); + auth.processAuthenticatePersona(pin6); + } + + byte[] ret = signer.SELECT(); + + String wallet1_privk + = "bd65457f5f410787c5bcbaa2d97a5ec1cd7a2e0315fd33c52b361a207eec3123"; + Credentials wallet1 = Credentials.create(wallet1_privk); + String wallet1_address = wallet1.getAddress(); + + card.INITIALIZE_UPDATE(); + card.EXTERNAL_AUTHENTICATE((byte)(SCP02.C_DECRYPTION | SCP02.C_MAC)); + signer.processLoadKey(wallet1.getEcKeyPair()); + card.INITIALIZE_UPDATE(); + + String wallet2_privk + = "67c095f769ea8120c3ffd8d6054876986dcad016a386bc30fb176e77590adce0"; + Credentials wallet2 = Credentials.create(wallet2_privk); + String wallet2_address = wallet2.getAddress(); + + Web3j web3j = Web3j.build(new HttpService("http://localhost:8545")); + + // Verify balance + System.out.println( + "Wallet 1 balance: " + + web3j + .ethGetBalance(wallet1.getAddress(), + DefaultBlockParameterName.LATEST) + .send() + .getBalance()); + System.out.println( + "Wallet 2 balance: " + + web3j + .ethGetBalance(wallet2.getAddress(), + DefaultBlockParameterName.LATEST) + .send() + .getBalance()); + + // Create transaction + BigInteger gasPrice = web3j.ethGasPrice().send().getGasPrice(); + BigInteger weiValue + = Convert.toWei(BigDecimal.valueOf(1.0), Convert.Unit.FINNEY) + .toBigIntegerExact(); + BigInteger nonce + = web3j + .ethGetTransactionCount(wallet1.getAddress(), + DefaultBlockParameterName.LATEST) + .send() + .getTransactionCount(); + + RawTransaction rawTransaction + = RawTransaction.createEtherTransaction(nonce, + gasPrice, + Transfer.GAS_LIMIT, + wallet2.getAddress(), + weiValue); + + // Sign transaction + byte[] txBytes = TransactionEncoder.encode(rawTransaction); + Sign.SignatureData signature = signMessage(txBytes, signer); + + Method encode = TransactionEncoder.class.getDeclaredMethod( + "encode", RawTransaction.class, Sign.SignatureData.class); + encode.setAccessible(true); + + // Send transaction + byte[] signedMessage + = (byte[])encode.invoke(null, rawTransaction, signature); + String hexValue = "0x" + Hex.toHexString(signedMessage); + EthSendTransaction ethSendTransaction + = web3j.ethSendRawTransaction(hexValue).send(); + + if (ethSendTransaction.hasError()) { + System.out.println("Transaction Error: " + + ethSendTransaction.getError().getMessage()); + } + + Assert.assertFalse(ethSendTransaction.hasError()); + } + + private static Sign.SignatureData signMessage(byte[] message, + SignApplet signer) + throws Exception + { + byte[] messageHash = Hash.sha3(message); + byte[] rawSig = signer.sign(messageHash, 1); + Dump.print(rawSig); + + int rLen = rawSig[3]; + int sOff = 6 + rLen; + int sLen = rawSig.length - rLen - 6; + + BigInteger r = new BigInteger(Arrays.copyOfRange(rawSig, 4, 4 + rLen)); + BigInteger s + = new BigInteger(Arrays.copyOfRange(rawSig, sOff, sOff + sLen)); + + Class ecdsaSignature + = Class.forName("org.web3j.crypto.Sign$ECDSASignature"); + Constructor ecdsaSignatureConstructor + = ecdsaSignature.getDeclaredConstructor(BigInteger.class, + BigInteger.class); + ecdsaSignatureConstructor.setAccessible(true); + Object sig = ecdsaSignatureConstructor.newInstance(r, s); + Method m = ecdsaSignature.getMethod("toCanonicalised"); + m.setAccessible(true); + sig = m.invoke(sig); + + Method recoverFromSignature = Sign.class.getDeclaredMethod( + "recoverFromSignature", int.class, ecdsaSignature, byte[].class); + recoverFromSignature.setAccessible(true); + + byte[] pubData = signer.processGetPubKey(); + BigInteger publicKey + = new BigInteger(Arrays.copyOfRange(pubData, 1, pubData.length)); + + int recId = -1; + for (int i = 0; i < 4; i++) { + BigInteger k = (BigInteger)recoverFromSignature.invoke( + null, i, sig, messageHash); + if (k != null && k.equals(publicKey)) { + recId = i; + break; + } + } + if (recId == -1) { + throw new RuntimeException( + "Could not construct a recoverable key. This should never happen."); + } + + int headerByte = recId + 27; + + Field rF = ecdsaSignature.getDeclaredField("r"); + rF.setAccessible(true); + Field sF = ecdsaSignature.getDeclaredField("s"); + sF.setAccessible(true); + r = (BigInteger)rF.get(sig); + s = (BigInteger)sF.get(sig); + + // 1 header + 32 bytes for R + 32 bytes for S + byte v = (byte)headerByte; + byte[] rB = Numeric.toBytesPadded(r, 32); + byte[] sB = Numeric.toBytesPadded(s, 32); + + return new Sign.SignatureData(v, rB, sB); + } + + @Test public static void test_DataElement() throws CardException + { + UUID uuid = UUID.randomUUID(); + + byte[] b9 = { + (byte)0x01, + (byte)0x02, + (byte)0x03, + (byte)0x04, + (byte)0x05, + (byte)0x06, + (byte)0x07, + (byte)0x08, + (byte)0x09, + }; + + String ident = "John Doe"; + byte[] s = ident.getBytes(); + + // DataElement e0 = new DataElement(DataElement.PRIVATEKEY, b9); + DataElement e4 = new DataElement(DataElement.U_INT_4, 12345); + DataElement e1 = new DataElement(DataElement.STRING, ident); + DataElement e2 = new DataElement(DataElement.INT_1, 42); + DataElement e3 = new DataElement(DataElement.UUID, uuid); + + DataElement sequence = new DataElement(DataElement.DATSEQ); + // sequence.addElement(e0); + sequence.addElement(e4); + sequence.addElement(e1); + sequence.addElement(e2); + sequence.addElement(e3); + + byte[] de = sequence.toByteArray(); + + DataElement elem = null; + DataElement c = new DataElement(de); + elem = c; + + int count = 4; + for (Enumeration en = (Enumeration)c.getValue(); + en.hasMoreElements();) { + int t = elem.getDataType(); + switch (t) { + case DataElement.STRING: + byte[] arr = ((String)elem.getValue()).getBytes(); + Assert.assertTrue(Arrays.equals(arr, s)); + count--; + break; + + case DataElement.U_INT_4: + Assert.assertEquals((int)elem.getLong(), 12345); + count--; + break; + + case DataElement.INT_1: + Assert.assertEquals((byte)elem.getLong(), (byte)42); + count--; + break; + + /*case DataElement.PRIVATEKEY: + byte[] barr = (byte[])elem.getValue(); + Assert.assertTrue(Arrays.equals(barr, b9)); + count--; + break;*/ + + case DataElement.UUID: + count--; + break; + + case DataElement.DATSEQ: + count--; + break; + } + + elem = (DataElement)en.nextElement(); + } + + Assert.assertTrue(count == 0); + } + + @Test public static void test_DataElement_cardside() throws CardException + { + byte[] privkeybytes = Hex.decode("001234560000090807060504030201"); + byte[] pubkeybytes + = Hex.decode("FFEE0011223344557788990001C0FFEE010203040506070809"); + + /*DataElement privkey + = new DataElement(DataElement.PRIVATEKEY, privkeybytes);*/ + // DataElement pubkey = new DataElement(DataElement.PUBLICKEY, + // pubkeybytes); + + long unixTime = System.currentTimeMillis() / 1000L; + DataElement epochTime = new DataElement(DataElement.U_INT_4, unixTime); + DataElement comment = new DataElement( + DataElement.STRING, "Ethereum signing key"); // testing meta data + + DataElement keyUpdateData = new DataElement( + DataElement.DATSEQ); // data element sequence container + + // keyUpdateData.addElement(privkey); + // keyUpdateData.addElement(pubkey); + keyUpdateData.addElement(epochTime); + keyUpdateData.addElement(comment); + + byte[] data = keyUpdateData.toByteArray(); + Dump.print(data); + + OffCard card = OffCard.getInstance(/* Helper.getPcscChannel() */); + if (card != null) { + DecodeApplet x = (DecodeApplet)card.INSTALL(DecodeApplet.class); + x.SELECT(); + byte[] resp = x.ins_echo( + data, + 2, + 0); // pass number of blob in p1 for additional checking + Dump.print(resp); + + // DecodeApplet.java applet should echo back same blob + Assert.assertEquals(resp, data, "Data Element TLV Test"); + } + } +} diff --git a/sam b/sam index e7d77e6..bf6017c 160000 --- a/sam +++ b/sam @@ -1 +1 @@ -Subproject commit e7d77e6d28cd8989f41bb041793a9ae5e5cc42a2 +Subproject commit bf6017cd103528e90bbcf081683ff637ab39a71b diff --git a/scripts/1_SUCCESS_TEST.jcsh b/scripts/1_SUCCESS_TEST.jcsh index 21e3e3a..abf5c01 100644 --- a/scripts/1_SUCCESS_TEST.jcsh +++ b/scripts/1_SUCCESS_TEST.jcsh @@ -4,7 +4,8 @@ _set_vars # power reset. Channel not secured /atr -# select applet + +# select Auth applet S-AUTH # Secure Channel ENC level @@ -13,8 +14,12 @@ init-update ${keySet} ext-auth enc -# Add listener -AL ${samInstanceAID} +# Add sam listener +#AL ${samInstanceAID} + +# Add datastorage listener +AL ${datastorageInstanceAID} + # Add persona AP # Add authData for persona (pin or bio template) @@ -27,26 +32,31 @@ S-AUTH # Authenticate persona with candidate (entered pin or bio data) AUP ${candidate} -#/atr +/atr 2_TEST-SAM +#/atr +3_TEST-DATASTORAGE + # select Auth applet S-AUTH + # Secure Channel MAC level (because no input data - nothing to encrypt) _setkeys init-update ${keySet} ext-auth mac - # delete verifier template DVP 00 00 # delete persona DP 00 -# delete listener +# delete listeners init-update ${keySet} ext-auth enc -DL ${samInstanceAID} +#DL ${samInstanceAID} +DL ${datastorageInstanceAID} + /echo "#####################################################" /echo "SUCCESS TEST DONE" -/echo "#####################################################" \ No newline at end of file +/echo "#####################################################" diff --git a/scripts/2_TEST-SAM.jcsh b/scripts/2_TEST-SAM.jcsh index 3e003ee..97bd40e 100644 --- a/scripts/2_TEST-SAM.jcsh +++ b/scripts/2_TEST-SAM.jcsh @@ -1,7 +1,8 @@ /echo "#####################################################" -/echo "SUCCESS TEST START" +/echo "SUCCESS TEST SAM START" /echo "#####################################################" +#/select F764656D6F0101 /select ${samInstanceAID} /s-v openedSlot ${response;s0,$(/expr ${response;l} - 4)} /echo Opened Slot: ${openedSlot} @@ -27,4 +28,8 @@ /send "00DC0000${length;h6}${outData}" /s-v outData ${response;s0,$(/expr ${response;l} - 4)} -/echo Decrypt Result: ${outData} \ No newline at end of file +/echo Decrypt Result: ${outData} + +/echo "#####################################################" +/echo "SUCCESS TEST SAM END" +/echo "#####################################################" \ No newline at end of file diff --git a/scripts/3_TEST-DATASTORAGE.jcsh b/scripts/3_TEST-DATASTORAGE.jcsh new file mode 100644 index 0000000..9b3f01e --- /dev/null +++ b/scripts/3_TEST-DATASTORAGE.jcsh @@ -0,0 +1,50 @@ +/echo "#####################################################" +/echo "SUCCESS DATASTORAGE TEST START" +/echo "#####################################################" + +/echo Select datastorageInstanceAID +/select ${datastorageInstanceAID} +/s-v currentVirtualCardId ${response;s0,$(/expr ${response;l} - 4)} +/echo Current VirtualCard Id: ${currentVirtualCardId} + +/echo Switch 1 +/send "009C0000#()" +/s-v outData ${response;s0,$(/expr ${response;l} - 4)} +/echo Current VirtualCard Id: ${outData} + +/echo Switch 2 +/send "009C0000#()" +/s-v outData ${response;s0,$(/expr ${response;l} - 4)} +/echo Current VirtualCard Id: ${outData} + +/echo Switch 3 +/send "009C0000#()" +/s-v outData ${response;s0,$(/expr ${response;l} - 4)} +/echo Current VirtualCard Id: ${outData} + +/echo Select datastorageInstanceAID +/select ${datastorageInstanceAID} +/s-v currentVirtualCardId ${response;s0,$(/expr ${response;l} - 4)} +/echo Current VirtualCard Id: ${currentVirtualCardId} + +/echo Switch 4 +/send "009C0000#()" +/s-v outData ${response;s0,$(/expr ${response;l} - 4)} +/echo Current VirtualCard Id: ${outData} + +/echo card reset - close access to virtual cards +/atr + +/echo Select datastorageInstanceAID +/select ${datastorageInstanceAID} +/s-v currentVirtualCardId ${response;s0,$(/expr ${response;l} - 4)} +/echo Current VirtualCard Id: ${currentVirtualCardId} + +/echo Switch 5 +/send "009C0000#()" +/s-v outData ${response;s0,$(/expr ${response;l} - 4)} +/echo Current VirtualCard Id: ${outData} + +/echo "#####################################################" +/echo "SUCCESS DATASTORAGE TEST END" +/echo "#####################################################" \ No newline at end of file diff --git a/scripts/DELETE-ALL.jcsh b/scripts/DELETE-ALL.jcsh index 67bf7c0..a35dfc2 100644 --- a/scripts/DELETE-ALL.jcsh +++ b/scripts/DELETE-ALL.jcsh @@ -7,6 +7,9 @@ _setkeys _auth +/set-var -g packageAID ${datastoragePackageAID} +_delete + /set-var -g packageAID ${samPackageAID} _delete diff --git a/scripts/INSTALL-DATASTORAGE.jcsh b/scripts/INSTALL-DATASTORAGE.jcsh new file mode 100644 index 0000000..0c5e66b --- /dev/null +++ b/scripts/INSTALL-DATASTORAGE.jcsh @@ -0,0 +1,14 @@ +/mode echo=off + +_set_vars + +/set-var -g instanceAID ${datastorageInstanceAID} +/set-var -g installParams ${datastorageInstallParams} +/set-var -g packageAID ${datastoragePackageAID} +/set-var -g appletAID ${datastorageAppletAID} + +INSTALL + +/mode echo=off +/echo +/echo "Done" diff --git a/scripts/UIA.jcsh b/scripts/UIA.jcsh index 271275d..590c77e 100644 --- a/scripts/UIA.jcsh +++ b/scripts/UIA.jcsh @@ -1,3 +1,4 @@ UPLOAD INSTALL-AUTH -INSTALL-SAM \ No newline at end of file +INSTALL-SAM +INSTALL-DATASTORAGE \ No newline at end of file diff --git a/scripts/UPLOAD.jcsh b/scripts/UPLOAD.jcsh index caf4319..a4be1aa 100644 --- a/scripts/UPLOAD.jcsh +++ b/scripts/UPLOAD.jcsh @@ -25,6 +25,9 @@ upload -b 248 ${appletAuthName} /cap-info ${appletSamName} upload -b 248 ${appletSamName} +/cap-info ${appletDatastorageName} +upload -b 248 ${appletDatastorageName} + /mode echo=off /echo /echo "#####################################################" diff --git a/scripts/_set_vars.jcsh b/scripts/_set_vars.jcsh index b7ea423..4ea8a91 100644 --- a/scripts/_set_vars.jcsh +++ b/scripts/_set_vars.jcsh @@ -6,26 +6,39 @@ /echo /mode echo=off +# CardManager AIDs /set-var -g sdAID1 a000000003000000 /set-var -g sdAID2 A000000151000000 -/set-var -g appletToolsName ${path}"../tools/bin/org/idpass/tools/javacard/tools.cap" +# tools applet +/set-var -g appletToolsName ${path}"../build/javacard/tools.cap" /set-var -g toolsPackageAID F7|idpass|00 -/set-var -g appletAuthName ${path}"../auth/bin/org/idpass/auth/javacard/auth.cap" +# auth applet +/set-var -g appletAuthName ${path}"../build/javacard/auth.cap" /set-var -g authPackageAID F7|idpass|01 -/set-var appversion 0001 -/set-var appid 01 -/set-var -g authAppletAID ${authPackageAID}${appid}${appversion} +/set-var authAppVersion 0001 +/set-var authAppId 01 +/set-var -g authAppletAID ${authPackageAID}${authAppId}${authAppVersion} /set-var -g authInstanceAID ${authAppletAID}01 -/set-var -g appletSamName ${path}"../sam/bin/org/idpass/sam/javacard/sam.cap" +# sam applet +/set-var -g appletSamName ${path}"../build/javacard/sam.cap" /set-var -g samPackageAID F7|idpass|02 -/set-var appversion 0001 -/set-var appid 01 -/set-var -g samAppletAID ${samPackageAID}${appid}${appversion} +/set-var samAppVersion 0001 +/set-var samAppId 01 +/set-var -g samAppletAID ${samPackageAID}${samAppId}${samAppVersion} /set-var -g samInstanceAID ${samAppletAID}01 +# datastorage applet +/set-var -g appletDatastorageName ${path}"../build/javacard/datastorage.cap" +/set-var -g datastoragePackageAID F7|idpass|03 +/set-var datastorageAppVersion 0001 +/set-var datastorageAppId 01 +/set-var -g datastorageAppletAID ${datastoragePackageAID}${datastorageAppId}${datastorageAppVersion} +/set-var -g datastorageInstanceAID ${datastorageAppletAID}01 + + /set-var -g extAuthLevel mac # PIN mock @@ -124,6 +137,7 @@ end /set-var -g samInstallParams ${secret} +/set-var -g datastorageInstallParams ${secret} /mode echo=off /echo /echo "Done" diff --git a/scripts/_setkeys.jcsh b/scripts/_setkeys.jcsh index 48d8ec1..9a7c67e 100644 --- a/scripts/_setkeys.jcsh +++ b/scripts/_setkeys.jcsh @@ -14,4 +14,4 @@ set-key ${keySet}/1/DES-ECB/404142434445464748494a4b4c4d4e4f ${keySet}/2/DES-ECB /mode echo=off /echo -/echo "Done" \ No newline at end of file +/echo "Done" diff --git a/settings.gradle b/settings.gradle index 4c2aaf6..5f5c644 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,3 +2,8 @@ include 'tools' include 'auth' include 'sam' include 'datastorage' +include 'sign' + +include 'offcard' +include 'offcard:src:main:java:org:idpass:dev' + diff --git a/sign b/sign new file mode 160000 index 0000000..07ed94c --- /dev/null +++ b/sign @@ -0,0 +1 @@ +Subproject commit 07ed94c500c642d172002e83e4e5214680e51e88 diff --git a/tools b/tools index 88adca2..8070513 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit 88adca2cbb80038a00efab340f41cfb2dd21a20b +Subproject commit 807051312f95a4463058c784bdf11847b60efe98