diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..c264ec3 --- /dev/null +++ b/.classpath @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 192221b..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.gradle/ -build/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index ab62c5b..0000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "libs-sdks"] - path = libs-sdks - url = https://github.com/martinpaljak/oracle_javacard_sdks.git -[submodule "libs"] - path = libs - url = https://github.com/J08nY/javacard-libs diff --git a/.project b/.project new file mode 100644 index 0000000..c199889 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + u2f + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 93681be..0000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: java - -jdk: - - oraclejdk8 - -script: - - ./gradlew check --info - - ./gradlew buildJavaCard --info - - ./gradlew jacocoTestReport - -after_success: - - bash <(curl -s https://codecov.io/bash) diff --git a/bin/de/asdfjkl/u2f/javacard/FIDOAPI.class b/bin/de/asdfjkl/u2f/javacard/FIDOAPI.class new file mode 100644 index 0000000..e12caf6 Binary files /dev/null and b/bin/de/asdfjkl/u2f/javacard/FIDOAPI.class differ diff --git a/bin/de/asdfjkl/u2f/javacard/FIDOStandalone.class b/bin/de/asdfjkl/u2f/javacard/FIDOStandalone.class new file mode 100644 index 0000000..440ab7a Binary files /dev/null and b/bin/de/asdfjkl/u2f/javacard/FIDOStandalone.class differ diff --git a/bin/de/asdfjkl/u2f/javacard/FIDOUtils.class b/bin/de/asdfjkl/u2f/javacard/FIDOUtils.class new file mode 100644 index 0000000..c79983e Binary files /dev/null and b/bin/de/asdfjkl/u2f/javacard/FIDOUtils.class differ diff --git a/bin/de/asdfjkl/u2f/javacard/Secp256r1.class b/bin/de/asdfjkl/u2f/javacard/Secp256r1.class new file mode 100644 index 0000000..ea7bd0b Binary files /dev/null and b/bin/de/asdfjkl/u2f/javacard/Secp256r1.class differ diff --git a/bin/de/asdfjkl/u2f/javacard/U2FApplet.class b/bin/de/asdfjkl/u2f/javacard/U2FApplet.class new file mode 100644 index 0000000..efe201b Binary files /dev/null and b/bin/de/asdfjkl/u2f/javacard/U2FApplet.class differ diff --git a/build.gradle b/build.gradle deleted file mode 100644 index bbc94e3..0000000 --- a/build.gradle +++ /dev/null @@ -1,96 +0,0 @@ -group 'ledger-u2f-javacard' -version '1.0-SNAPSHOT' - -// Buildscript configuration for the javacard-gradle plugin. -// Do not modify this particular block. Dependencies for the project are lower. -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath 'com.klinec:gradle-javacard:1.5.5' - } -} - -apply plugin: 'javacard' -apply plugin: 'jacoco' -sourceCompatibility = 1.7 - -// Common settings, definitions -final def rootPath = rootDir.absolutePath -final def libs = rootPath + '/libs' -final def libsSdk = rootPath + '/libs-sdks' - -// Repositories for your project -repositories { - mavenCentral() - // mavenLocal() // for local maven repository if needed - flatDir { - dirs libs - } -} - -// Dependencies for your project -dependencies { - // testCompile group: 'junit', name: 'junit', version: '4.12' - // testCompile 'org.testng:testng:6.1.1' - - jcardsim 'com.licel:jcardsim:3.0.4' -} - -test { - useTestNG() - jvmArgs '-noverify' -} - -// JavaCard SDKs and libraries -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 JC305 = libsSdk + '/jc305u1_kit' - -// Which JavaCard SDK to use - select -final def JC_SELECTED = JC304 - -javacard { - - //noinspection GroovyAssignabilityCheck - config { - jckit JC_SELECTED - - // JCardSim automatically added by the javacard-gradle plugin - addSurrogateJcardSimRepo true - addImplicitJcardSim true - addImplicitJcardSimJunit true - - //noinspection GroovyAssignabilityCheck - cap { - packageName 'com.ledger.u2f' - version '1.1' - aid '0xa0:0x00:0x00:0x06:0x17:0x00:0x4f:0x97:0xa2:0xe9:0x50:0x01' - output 'ledger-u2f.cap' - - //noinspection GroovyAssignabilityCheck - applet { - className 'U2FApplet' - aid '0xa0:0x00:0x00:0x06:0x17:0x00:0x4f:0x97:0xa2:0xe9:0x49:0x01' - } - - //noinspection GroovyAssignabilityCheck - dependencies { - remote 'fr.bmartel:gplatform:2.1.1' - } - } - } -} - -jacocoTestReport { - reports { - xml.enabled true - html.enabled false - } -} - -check.dependsOn jacocoTestReport diff --git a/build.xml b/build.xml index bc20660..f976a94 100644 --- a/build.xml +++ b/build.xml @@ -1,12 +1,27 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/cap/u2f.cap b/cap/u2f.cap new file mode 100644 index 0000000..7bebd3e Binary files /dev/null and b/cap/u2f.cap differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 02716d7..0000000 Binary files a/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index a8e774d..0000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Sun Dec 10 20:07:32 CET 2017 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-all.zip diff --git a/gradlew b/gradlew deleted file mode 100755 index 91a7e26..0000000 --- a/gradlew +++ /dev/null @@ -1,164 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index aec9973..0000000 --- a/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/libs b/libs deleted file mode 160000 index 6bda0b1..0000000 --- a/libs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6bda0b11aa663b24bcad4d39a3a673e0c5d3564d diff --git a/libs-sdks b/libs-sdks deleted file mode 160000 index 2b36942..0000000 --- a/libs-sdks +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2b369428203976e7bec9dee4cfc5fd06b8518864 diff --git a/src/main/java/com/ledger/u2f/FIDOAPI.java b/src/de/asdfjkl/u2f/javacard/FIDOAPI.java similarity index 97% rename from src/main/java/com/ledger/u2f/FIDOAPI.java rename to src/de/asdfjkl/u2f/javacard/FIDOAPI.java index 0637d9f..20c145f 100644 --- a/src/main/java/com/ledger/u2f/FIDOAPI.java +++ b/src/de/asdfjkl/u2f/javacard/FIDOAPI.java @@ -2,7 +2,8 @@ ******************************************************************************* * FIDO U2F Authenticator * (c) 2015 Ledger - * + * (c) 2022 Dominik Klein + * * Licensed 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 @@ -17,7 +18,7 @@ ******************************************************************************* */ -package com.ledger.u2f; +package de.asdfjkl.u2f.javacard; import javacard.security.ECPrivateKey; diff --git a/src/de/asdfjkl/u2f/javacard/FIDOStandalone.java b/src/de/asdfjkl/u2f/javacard/FIDOStandalone.java new file mode 100644 index 0000000..cc2396b --- /dev/null +++ b/src/de/asdfjkl/u2f/javacard/FIDOStandalone.java @@ -0,0 +1,223 @@ +/* + ******************************************************************************* + * FIDO U2F Authenticator + * (c) 2015 Ledger + * (c) 2022 Dominik Klein + * + * Licensed 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. + ******************************************************************************* + */ + +package de.asdfjkl.u2f.javacard; + +import javacard.framework.ISO7816; +import javacard.framework.ISOException; +import javacard.framework.JCSystem; +import javacard.security.RandomData; +import javacard.framework.Util; +import javacard.security.*; +import javacardx.crypto.Cipher; + +public class FIDOStandalone implements FIDOAPI { + + private KeyPair keyPair; + private AESKey chipKey; + private AESKey obfuscationKey; + private Cipher obfuscationCipherEncrypt; + private Cipher cipherEncrypt; + private Cipher cipherDecrypt; + private Cipher obfuscationCipherDecrypt; + private RandomData random; + private byte[] scratch; + private short[] shuffleAccess; + private byte[] scratchRandom; + + private static final byte[] IV_ZERO_AES = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + + /** + * Init cipher engines and allocate memory. + */ + public FIDOStandalone(byte[] aesKey, short aesKeyOffset) { + scratch = JCSystem.makeTransientByteArray((short) 64, JCSystem.CLEAR_ON_DESELECT); + scratchRandom = JCSystem.makeTransientByteArray((short) 64, JCSystem.CLEAR_ON_DESELECT); + shuffleAccess = JCSystem.makeTransientShortArray(((short) 32), JCSystem.CLEAR_ON_DESELECT); + for(short i = 0;i < 32;i++) { + shuffleAccess[i] = i; + } + keyPair = new KeyPair( + (ECPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, KeyBuilder.LENGTH_EC_FP_256, false), + (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, KeyBuilder.LENGTH_EC_FP_256, false)); + Secp256r1.setCommonCurveParameters((ECKey) keyPair.getPrivate()); + Secp256r1.setCommonCurveParameters((ECKey) keyPair.getPublic()); + random = RandomData.getInstance(RandomData.ALG_SECURE_RANDOM); + // Initialize the unique wrapping key + chipKey = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES, KeyBuilder.LENGTH_AES_256, false); + //chipKey.setKey(scratch, (short) 0); + obfuscationKey = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES, KeyBuilder.LENGTH_AES_256, false); + chipKey.setKey(aesKey, aesKeyOffset); + cipherEncrypt = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_CBC_NOPAD, false); + cipherEncrypt.init(chipKey, Cipher.MODE_ENCRYPT, IV_ZERO_AES, (short) 0, (short) IV_ZERO_AES.length); + + obfuscationCipherEncrypt = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_CBC_NOPAD, false); + + cipherDecrypt = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_CBC_NOPAD, false); + cipherDecrypt.init(chipKey, Cipher.MODE_DECRYPT, IV_ZERO_AES, (short) 0, (short) IV_ZERO_AES.length); + + obfuscationCipherDecrypt = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_CBC_NOPAD, false); + } + + /** + * Randomly shuffle the input array using Fisher-Yates Algorithm + * @param array The array to be shuffled, must be of length 32 + */ + private void randomShuffle32(short[] array) { + + random.generateData(scratchRandom, (short) 0, (short) 32); + + for(short i=31;i>=1;i--) { + + short j = (short) (((short) scratchRandom[i] & 0xff) % ((short) (i+1))); + short tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + } + } + + /** + * Interleave two byte arrays into the target one, nibble by nibble. + * Example: + * array1 = [0x12, 0x34] + * array2 = [0xab, 0xcd] + * -> [0x1a, 0x2b, 0x3c, 0x4d] + *

+ * This is used to interleave the generated private key and the application parameter into two AES-CBC blocks, + * as not doing so would result in the application parameter being encrypted as a block with an all zero IV which + * would always result in the same first block for all generated private keys with the same application parameter + * wrapped under the same wrapping key, which would break privacy of U2F. + * + * @param array1 + * @param array1Offset + * @param array2 + * @param array2Offset + * @param target + * @param targetOffset + * @param length + */ + private static void interleave32(byte[] array1, short array1Offset, byte[] array2, short array2Offset, byte[] target, short targetOffset, short[] permutation) { + for (short c = 0; c < 32; c++) { + short i = permutation[c]; + short a = (short) (array1[(short) (array1Offset + i)] & 0xff); + short b = (short) (array2[(short) (array2Offset + i)] & 0xff); + target[(short) (targetOffset + 2 * i)] = (byte) ((short) (a & 0xf0) | (short) (b >> 4)); + target[(short) (targetOffset + 2 * i + 1)] = (byte) ((short) ((a & 0x0f) << 4) | (short) (b & 0x0f)); + } + } + + /** + * Deinterleave a byte array back into two arrays of half size. + * Example: + * src = [0x1a, 0x2b, 0x3c, 0x4d] + * -> [0x12, 0x34] and [0xab, 0xcd] + * + * @param src + * @param srcOffset + * @param array1 + * @param array1Offset + * @param array2 + * @param array2Offset + * @param length + */ + private static void deinterleave32(byte[] src, short srcOffset, byte[] array1, short array1Offset, byte[] array2, short array2Offset, short[] permutation) { + for (short c = 0; c < 32; c++) { + short i = permutation[c]; + short a = (short) (src[(short) (srcOffset + 2 * i)] & 0xff); + short b = (short) (src[(short) (srcOffset + 2 * i + 1)] & 0xff); + array1[(short) (array1Offset + i)] = (byte) ((short) (a & 0xf0) | (short) (b >> 4)); + array2[(short) (array2Offset + i)] = (byte) (((short) (a & 0x0f) << 4) | (short) (b & 0x0f)); + } + } + + /* @override */ + public short generateKeyAndWrap(byte[] applicationParameter, short applicationParameterOffset, ECPrivateKey generatedPrivateKey, byte[] publicKey, short publicKeyOffset, byte[] keyHandle, short keyHandleOffset) { + // Generate a new pair + keyPair.genKeyPair(); + // Copy public key + ((ECPublicKey) keyPair.getPublic()).getW(publicKey, publicKeyOffset); + // Wrap keypair and application parameters + ((ECPrivateKey) keyPair.getPrivate()).getS(scratch, (short) 0); + + // generate random access table + for(short i = 0;i < 32;i++) { + shuffleAccess[i] = i; + } + randomShuffle32(shuffleAccess); + interleave32(applicationParameter, applicationParameterOffset, scratch, (short) 0, keyHandle, keyHandleOffset, shuffleAccess); + + // add dummy rounds for AES encryption + short doEncrypt = (short) (shuffleAccess[0] % 4); + for(short i = 0;i<4;i++) { + random.generateData(scratchRandom, (short) 0, (short) 32); + obfuscationKey.setKey(scratchRandom, (short) 0); + obfuscationCipherEncrypt.init(obfuscationKey, Cipher.MODE_ENCRYPT, IV_ZERO_AES, (short) 0, (short) IV_ZERO_AES.length); + + if(i == doEncrypt) { + cipherEncrypt.doFinal(keyHandle, keyHandleOffset, (short) 64, keyHandle, keyHandleOffset); + } else { + obfuscationCipherEncrypt.doFinal(scratchRandom, (short) 0, (short) 64, scratchRandom, (short) 0); + } + + } + Util.arrayFillNonAtomic(scratch, (short) 0, (short) 32, (byte) 0x00); + return (short) 64; + } + + /* @override */ + public boolean unwrap(byte[] keyHandle, short keyHandleOffset, short keyHandleLength, byte[] applicationParameter, short applicationParameterOffset, ECPrivateKey unwrappedPrivateKey) { + + // our random shuffled array will be used both for dummy decryption + // as well as randomly deinterleave the keyHandle + for(short i = 0;i < 32;i++) { + shuffleAccess[i] = i; + } + randomShuffle32(shuffleAccess); + + // add dummy rounds for AES decryption + short doDecrypt = (short) (shuffleAccess[0] % 4); + for(short i = 0;i<4;i++) { + random.generateData(scratchRandom, (short) 0, (short) 32); + obfuscationKey.setKey(scratchRandom, (short) 0); + obfuscationCipherDecrypt.init(obfuscationKey, Cipher.MODE_DECRYPT, IV_ZERO_AES, (short) 0, (short) IV_ZERO_AES.length); + if(i == doDecrypt) { + cipherDecrypt.doFinal(keyHandle, keyHandleOffset, (short) 64, keyHandle, keyHandleOffset); + } else { + obfuscationCipherDecrypt.doFinal(scratchRandom, (short) 0, (short) 64, scratchRandom, (short) 0); + } + + } + + // Verify + deinterleave32(keyHandle, keyHandleOffset, scratch, (short) 0, scratch, (short) 32, shuffleAccess); + if (!FIDOUtils.compareConstantTime(applicationParameter, applicationParameterOffset, scratch, (short) 0, (short) 32)) { + Util.arrayFillNonAtomic(scratch, (short) 32, (short) 32, (byte) 0x00); + Util.arrayFillNonAtomic(keyHandle, keyHandleOffset, (short) 64, (byte) 0x00); + return false; + } + Util.arrayFillNonAtomic(keyHandle, keyHandleOffset, (short) 64, (byte) 0x00); + if (unwrappedPrivateKey != null) { + unwrappedPrivateKey.setS(scratch, (short) 32, (short) 32); + } + Util.arrayFillNonAtomic(scratch, (short) 32, (short) 32, (byte) 0x00); + return true; + } + +} diff --git a/src/main/java/com/ledger/u2f/FIDOUtils.java b/src/de/asdfjkl/u2f/javacard/FIDOUtils.java similarity index 96% rename from src/main/java/com/ledger/u2f/FIDOUtils.java rename to src/de/asdfjkl/u2f/javacard/FIDOUtils.java index 7d58555..6806846 100644 --- a/src/main/java/com/ledger/u2f/FIDOUtils.java +++ b/src/de/asdfjkl/u2f/javacard/FIDOUtils.java @@ -2,6 +2,7 @@ ******************************************************************************* * FIDO U2F Authenticator * (c) 2015 Ledger + * (c) 2022 Dominik Klein * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +18,7 @@ ******************************************************************************* */ -package com.ledger.u2f; +package de.asdfjkl.u2f.javacard; /** * Utlity functions. diff --git a/src/main/java/com/ledger/u2f/Secp256r1.java b/src/de/asdfjkl/u2f/javacard/Secp256r1.java similarity index 96% rename from src/main/java/com/ledger/u2f/Secp256r1.java rename to src/de/asdfjkl/u2f/javacard/Secp256r1.java index 857b34a..b2876d8 100644 --- a/src/main/java/com/ledger/u2f/Secp256r1.java +++ b/src/de/asdfjkl/u2f/javacard/Secp256r1.java @@ -2,6 +2,7 @@ ******************************************************************************* * FIDO U2F Authenticator * (c) 2015 Ledger + * (c) 2022 Dominik Klein * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +18,7 @@ ******************************************************************************* */ -package com.ledger.u2f; +package de.asdfjkl.u2f.javacard; import javacard.security.ECKey; @@ -88,4 +89,8 @@ protected static boolean setCommonCurveParameters(ECKey key) { } } + + protected static void setCurveParamA(ECKey key) { + key.setA(SECP256R1_A, (short) 0, (short) SECP256R1_A.length); + } } diff --git a/src/main/java/com/ledger/u2f/U2FApplet.java b/src/de/asdfjkl/u2f/javacard/U2FApplet.java similarity index 95% rename from src/main/java/com/ledger/u2f/U2FApplet.java rename to src/de/asdfjkl/u2f/javacard/U2FApplet.java index acc1821..fecc741 100644 --- a/src/main/java/com/ledger/u2f/U2FApplet.java +++ b/src/de/asdfjkl/u2f/javacard/U2FApplet.java @@ -2,6 +2,7 @@ ******************************************************************************* * FIDO U2F Authenticator * (c) 2015 Ledger + * (c) 2022 Dominik Klein * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +18,7 @@ ******************************************************************************* */ -package com.ledger.u2f; +package de.asdfjkl.u2f.javacard; import javacard.framework.*; import javacard.security.CryptoException; @@ -40,6 +41,7 @@ public class U2FApplet extends Applet implements ExtendedLength { private ECPrivateKey attestationPrivateKey; private ECPrivateKey localPrivateKey; private boolean localPrivateTransient; + private boolean localPrivateTransient1; private boolean counterOverflowed; private Signature attestationSignature; private Signature localSignature; @@ -53,8 +55,8 @@ public class U2FApplet extends Applet implements ExtendedLength { private static final byte FIDO_INS_VERSION = (byte) 0x03; private static final byte ISO_INS_GET_DATA = (byte) 0xC0; - private static final byte PROPRIETARY_CLA = (byte) 0xF0; - private static final byte FIDO_ADM_SET_ATTESTATION_CERT = (byte) 0x01; + private static final byte PROPRIETARY_CLA = (byte) 0x80; + private static final byte FIDO_ADM_SET_ATTESTATION_CERT = (byte) 0x09; private static final byte SCRATCH_TRANSPORT_STATE = (byte) 0; private static final byte SCRATCH_CURRENT_OFFSET = (byte) 1; @@ -107,12 +109,23 @@ public class U2FApplet extends Applet implements ExtendedLength { * @param parametersLength always 35 */ public U2FApplet(byte[] parameters, short parametersOffset, byte parametersLength) { - if (parametersLength != 35) { + if (parametersLength != (35+32)) { ISOException.throwIt(ISO7816.SW_WRONG_DATA); } counter = new byte[4]; scratchPersistent = JCSystem.makeTransientByteArray((short) 1, JCSystem.CLEAR_ON_RESET); scratch = JCSystem.makeTransientByteArray((short) (SCRATCH_PAD + SCRATCH_PAD_SIZE), JCSystem.CLEAR_ON_DESELECT); + + localPrivateKey = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, KeyBuilder.LENGTH_EC_FP_256, false); + Secp256r1.setCommonCurveParameters(localPrivateKey); + + //localPrivateKey = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE_TRANSIENT_DESELECT, KeyBuilder.LENGTH_EC_FP_256, false); + //localPrivateTransient = true; + + //localPrivateKey = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE_TRANSIENT_RESET, KeyBuilder.LENGTH_EC_FP_256, false); + //localPrivateTransient = true; + + /* try { // ok, let's save RAM localPrivateKey = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE_TRANSIENT_DESELECT, KeyBuilder.LENGTH_EC_FP_256, false); @@ -127,7 +140,7 @@ public U2FApplet(byte[] parameters, short parametersOffset, byte parametersLengt localPrivateKey = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, KeyBuilder.LENGTH_EC_FP_256, false); Secp256r1.setCommonCurveParameters(localPrivateKey); } - } + }*/ attestationSignature = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false); localSignature = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false); flags = parameters[parametersOffset]; @@ -136,7 +149,7 @@ public U2FApplet(byte[] parameters, short parametersOffset, byte parametersLengt Secp256r1.setCommonCurveParameters(attestationPrivateKey); attestationPrivateKey.setS(parameters, (short) (parametersOffset + 3), (short) 32); attestationSignature.init(attestationPrivateKey, Signature.MODE_SIGN); - fidoImpl = new FIDOStandalone(); + fidoImpl = new FIDOStandalone(parameters, (short) (parametersOffset + 35)); } /** @@ -454,4 +467,3 @@ public static void install(byte bArray[], short bOffset, byte bLength) throws IS new U2FApplet(bArray, (short) (offset + 1), bArray[offset]).register(bArray, (short) (bOffset + 1), bArray[bOffset]); } } - diff --git a/src/main/java/com/ledger/u2f/FIDOStandalone.java b/src/main/java/com/ledger/u2f/FIDOStandalone.java deleted file mode 100644 index 61d0806..0000000 --- a/src/main/java/com/ledger/u2f/FIDOStandalone.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - ******************************************************************************* - * FIDO U2F Authenticator - * (c) 2015 Ledger - * - * Licensed 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. - ******************************************************************************* - */ - -package com.ledger.u2f; - -import javacard.framework.JCSystem; -import javacard.security.RandomData; -import javacard.framework.Util; -import javacard.security.*; -import javacardx.crypto.Cipher; - -public class FIDOStandalone implements FIDOAPI { - - private KeyPair keyPair; - private AESKey chipKey; - private Cipher cipherEncrypt; - private Cipher cipherDecrypt; - private RandomData random; - private byte[] scratch; - - private static final byte[] IV_ZERO_AES = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - - /** - * Init cipher engines and allocate memory. - */ - public FIDOStandalone() { - scratch = JCSystem.makeTransientByteArray((short) 64, JCSystem.CLEAR_ON_DESELECT); - keyPair = new KeyPair( - (ECPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, KeyBuilder.LENGTH_EC_FP_256, false), - (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, KeyBuilder.LENGTH_EC_FP_256, false)); - Secp256r1.setCommonCurveParameters((ECKey) keyPair.getPrivate()); - Secp256r1.setCommonCurveParameters((ECKey) keyPair.getPublic()); - random = RandomData.getInstance(RandomData.ALG_KEYGENERATION); - // Initialize the unique wrapping key - chipKey = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES, KeyBuilder.LENGTH_AES_256, false); - random.nextBytes(scratch, (short) 0, (short) 32); - chipKey.setKey(scratch, (short) 0); - cipherEncrypt = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_CBC_NOPAD, false); - cipherEncrypt.init(chipKey, Cipher.MODE_ENCRYPT, IV_ZERO_AES, (short) 0, (short) IV_ZERO_AES.length); - cipherDecrypt = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_CBC_NOPAD, false); - cipherDecrypt.init(chipKey, Cipher.MODE_DECRYPT, IV_ZERO_AES, (short) 0, (short) IV_ZERO_AES.length); - } - - /** - * Interleave two byte arrays into the target one, nibble by nibble. - * Example: - * array1 = [0x12, 0x34] - * array2 = [0xab, 0xcd] - * -> [0x1a, 0x2b, 0x3c, 0x4d] - *

- * This is used to interleave the generated private key and the application parameter into two AES-CBC blocks, - * as not doing so would result in the application parameter being encrypted as a block with an all zero IV which - * would always result in the same first block for all generated private keys with the same application parameter - * wrapped under the same wrapping key, which would break privacy of U2F. - * - * @param array1 - * @param array1Offset - * @param array2 - * @param array2Offset - * @param target - * @param targetOffset - * @param length - */ - private static void interleave(byte[] array1, short array1Offset, byte[] array2, short array2Offset, byte[] target, short targetOffset, short length) { - for (short i = 0; i < length; i++) { - short a = (short) (array1[(short) (array1Offset + i)] & 0xff); - short b = (short) (array2[(short) (array2Offset + i)] & 0xff); - target[(short) (targetOffset + 2 * i)] = (byte) ((short) (a & 0xf0) | (short) (b >> 4)); - target[(short) (targetOffset + 2 * i + 1)] = (byte) ((short) ((a & 0x0f) << 4) | (short) (b & 0x0f)); - } - } - - /** - * Deinterleave a byte array back into two arrays of half size. - * Example: - * src = [0x1a, 0x2b, 0x3c, 0x4d] - * -> [0x12, 0x34] and [0xab, 0xcd] - * - * @param src - * @param srcOffset - * @param array1 - * @param array1Offset - * @param array2 - * @param array2Offset - * @param length - */ - private static void deinterleave(byte[] src, short srcOffset, byte[] array1, short array1Offset, byte[] array2, short array2Offset, short length) { - for (short i = 0; i < length; i++) { - short a = (short) (src[(short) (srcOffset + 2 * i)] & 0xff); - short b = (short) (src[(short) (srcOffset + 2 * i + 1)] & 0xff); - array1[(short) (array1Offset + i)] = (byte) ((short) (a & 0xf0) | (short) (b >> 4)); - array2[(short) (array2Offset + i)] = (byte) (((short) (a & 0x0f) << 4) | (short) (b & 0x0f)); - } - } - - /* @override */ - public short generateKeyAndWrap(byte[] applicationParameter, short applicationParameterOffset, ECPrivateKey generatedPrivateKey, byte[] publicKey, short publicKeyOffset, byte[] keyHandle, short keyHandleOffset) { - // Generate a new pair - keyPair.genKeyPair(); - // Copy public key - ((ECPublicKey) keyPair.getPublic()).getW(publicKey, publicKeyOffset); - // Wrap keypair and application parameters - ((ECPrivateKey) keyPair.getPrivate()).getS(scratch, (short) 0); - interleave(applicationParameter, applicationParameterOffset, scratch, (short) 0, keyHandle, keyHandleOffset, (short) 32); - cipherEncrypt.doFinal(keyHandle, keyHandleOffset, (short) 64, keyHandle, keyHandleOffset); - Util.arrayFillNonAtomic(scratch, (short) 0, (short) 32, (byte) 0x00); - return (short) 64; - } - - /* @override */ - public boolean unwrap(byte[] keyHandle, short keyHandleOffset, short keyHandleLength, byte[] applicationParameter, short applicationParameterOffset, ECPrivateKey unwrappedPrivateKey) { - // Verify - cipherDecrypt.doFinal(keyHandle, keyHandleOffset, (short) 64, keyHandle, keyHandleOffset); - deinterleave(keyHandle, keyHandleOffset, scratch, (short) 0, scratch, (short) 32, (short) 32); - if (!FIDOUtils.compareConstantTime(applicationParameter, applicationParameterOffset, scratch, (short) 0, (short) 32)) { - Util.arrayFillNonAtomic(scratch, (short) 32, (short) 32, (byte) 0x00); - Util.arrayFillNonAtomic(keyHandle, keyHandleOffset, (short) 64, (byte) 0x00); - return false; - } - Util.arrayFillNonAtomic(keyHandle, keyHandleOffset, (short) 64, (byte) 0x00); - if (unwrappedPrivateKey != null) { - unwrappedPrivateKey.setS(scratch, (short) 32, (short) 32); - } - Util.arrayFillNonAtomic(scratch, (short) 32, (short) 32, (byte) 0x00); - return true; - } - -} diff --git a/state-model.png b/state-model.png deleted file mode 100644 index 6db5911..0000000 Binary files a/state-model.png and /dev/null differ diff --git a/tools/ant-javacard.jar b/tools/ant-javacard.jar new file mode 100644 index 0000000..739fd23 Binary files /dev/null and b/tools/ant-javacard.jar differ diff --git a/tools/gp.jar b/tools/gp.jar new file mode 100644 index 0000000..49267d7 Binary files /dev/null and b/tools/gp.jar differ diff --git a/tools/install_applet.bat b/tools/install_applet.bat new file mode 100644 index 0000000..bfca081 --- /dev/null +++ b/tools/install_applet.bat @@ -0,0 +1 @@ +java -jar gp.jar --reinstall ..\cap\u2f.cap -params 000140f3fccc0d00d8031954f90864d43c247f4bf5f0665c6b50cc17749a27d1cf7664 \ No newline at end of file diff --git a/tools/install_attestation.py b/tools/install_attestation.py new file mode 100644 index 0000000..27481c0 --- /dev/null +++ b/tools/install_attestation.py @@ -0,0 +1,51 @@ +from smartcard.Exceptions import NoCardException +from smartcard.System import readers +from smartcard.util import toHexString +import sys + +SEL_APPLET = [ 0x00,0xA4,0x04,0x00,0x08,0xA0,0x00,0x00,0x06,0x47,0x2F,0x00,0x01 ] +UPLOAD_1 = [ 0x80,0x09,0x00,0x00,0x80,0x30,0x82,0x01,0x3c,0x30,0x81,0xe4,0xa0,0x03,0x02,0x01,0x02,0x02,0x0a,0x47,0x90,0x12,0x80,0x00,0x11,0x55,0x95,0x73,0x52,0x30,0x0a,0x06,0x08,0x2a,0x86,0x48,0xce,0x3d,0x04,0x03,0x02,0x30,0x17,0x31,0x15,0x30,0x13,0x06,0x03,0x55,0x04,0x03,0x13,0x0c,0x47,0x6e,0x75,0x62,0x62,0x79,0x20,0x50,0x69,0x6c,0x6f,0x74,0x30,0x1e,0x17,0x0d,0x31,0x32,0x30,0x38,0x31,0x34,0x31,0x38,0x32,0x39,0x33,0x32,0x5a,0x17,0x0d,0x31,0x33,0x30,0x38,0x31,0x34,0x31,0x38,0x32,0x39,0x33,0x32,0x5a,0x30,0x31,0x31,0x2f,0x30,0x2d,0x06,0x03,0x55,0x04,0x03,0x13,0x26,0x50,0x69,0x6c,0x6f,0x74,0x47,0x6e,0x75,0x62,0x62,0x79,0x2d,0x30,0x2e,0x34,0x2e,0x31,0x2d,0x34,0x37,0x39,0x30 ] +UPLOAD_2 = [ 0x80,0x09,0x00,0x80,0x80,0x31,0x32,0x38,0x30,0x30,0x30,0x31,0x31,0x35,0x35,0x39,0x35,0x37,0x33,0x35,0x32,0x30,0x59,0x30,0x13,0x06,0x07,0x2a,0x86,0x48,0xce,0x3d,0x02,0x01,0x06,0x08,0x2a,0x86,0x48,0xce,0x3d,0x03,0x01,0x07,0x03,0x42,0x00,0x04,0x8d,0x61,0x7e,0x65,0xc9,0x50,0x8e,0x64,0xbc,0xc5,0x67,0x3a,0xc8,0x2a,0x67,0x99,0xda,0x3c,0x14,0x46,0x68,0x2c,0x25,0x8c,0x46,0x3f,0xff,0xdf,0x58,0xdf,0xd2,0xfa,0x3e,0x6c,0x37,0x8b,0x53,0xd7,0x95,0xc4,0xa4,0xdf,0xfb,0x41,0x99,0xed,0xd7,0x86,0x2f,0x23,0xab,0xaf,0x02,0x03,0xb4,0xb8,0x91,0x1b,0xa0,0x56,0x99,0x94,0xe1,0x01,0x30,0x0a,0x06,0x08,0x2a,0x86,0x48,0xce,0x3d,0x04,0x03,0x02,0x03,0x47,0x00,0x30,0x44,0x02,0x20,0x60,0xcd ] +UPLOAD_3 = [ 0x80,0x09,0x01,0x00,0x40,0xb6,0x06,0x1e,0x9c,0x22,0x26,0x2d,0x1a,0xac,0x1d,0x96,0xd8,0xc7,0x08,0x29,0xb2,0x36,0x65,0x31,0xdd,0xa2,0x68,0x83,0x2c,0xb8,0x36,0xbc,0xd3,0x0d,0xfa,0x02,0x20,0x63,0x1b,0x14,0x59,0xf0,0x9e,0x63,0x30,0x05,0x57,0x22,0xc8,0xd8,0x9b,0x7f,0x48,0x88,0x3b,0x90,0x89,0xb8,0x8d,0x60,0xd1,0xd9,0x79,0x59,0x02,0xb3,0x04,0x10,0xdf ] + +def print_response(response, sw1, sw2): + response_string = toHexString(response) + if(len(response) > 0): + print(response_string + ", sw1, sw2: "+ hex(sw1) + "," + hex(sw2)) + else: + print("sw1, sw2: " + hex(sw1) + "," + hex(sw2)) + + +for reader in readers(): + try: + connection = reader.createConnection() + connection.connect() + print(str(reader) + toHexString(connection.getATR())) + + apdu = SEL_APPLET + print("selecting applet...") + response, sw1, sw2 = connection.transmit(apdu) + print_response(response, sw1, sw2) + + apdu = UPLOAD_1 + print("upload 1...") + response, sw1, sw2 = connection.transmit(apdu) + print_response(response, sw1, sw2) + + apdu = UPLOAD_2 + print("upload 2...") + response, sw1, sw2 = connection.transmit(apdu) + print_response(response, sw1, sw2) + + apdu = UPLOAD_3 + print("upload 3...") + response, sw1, sw2 = connection.transmit(apdu) + print_response(response, sw1, sw2) + + + except NoCardException: + print(reader, 'no card inserted') + +if 'win32' == sys.platform: + print('press Enter to continue') + sys.stdin.read(1) \ No newline at end of file