├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── thainationalidcard ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── androidTest └── java │ └── co │ └── advancedlogic │ └── thainationalidcard │ └── ExampleInstrumentedTest.java ├── main ├── AndroidManifest.xml ├── java │ └── co │ │ └── advancedlogic │ │ └── thainationalidcard │ │ ├── PreciseEscapeObject.java │ │ ├── SmartCardDevice.java │ │ ├── SmartCardMessage.java │ │ └── ThaiSmartCard.java └── res │ └── values │ └── strings.xml └── test └── java └── co └── advancedlogic └── thainationalidcard └── ExampleUnitTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidThaiNationalIDCard 2 | Thai National ID Card library for Android application 3 | 4 | ##### How to install? 5 | - Add it in your root build.gradle at the end of repositories 6 | ``` 7 | allprojects { 8 | repositories { 9 | ... 10 | maven { url 'https://jitpack.io' } 11 | } 12 | } 13 | ``` 14 | - Add the dependency 15 | ``` 16 | dependencies { 17 | implementation 'com.github.Advanced-Logic:AndroidThaiNationalIDCard:1.0.3' 18 | } 19 | ``` 20 | ##### How to use? 21 | use this example code in button click event or somewhere after your activity is loaded. 22 | 23 | ```java 24 | SmartCardDevice device = SmartCardDevice.getSmartCardDevice(getApplicationContext(), "Smart Card", new SmartCardDevice.SmartCardDeviceEvent() { 25 | @Override 26 | public void OnReady(SmartCardDevice device) { 27 | ThaiSmartCard thaiSmartCard = new ThaiSmartCard(device); 28 | 29 | ThaiSmartCard.PersonalInformation info = thaiSmartCard.getPersonalInformation(); 30 | 31 | if (info == null) { 32 | Toast.makeText(getApplicationContext(), "Read Smart Card information failed", Toast.LENGTH_LONG).show(); 33 | return; 34 | } 35 | 36 | Log.d("SmartCard", String.format("PID: %s NameTH: %s NameEN: %s BirthDate: %s", info.PersonalID, info.NameTH, info.NameEN, info.BirthDate)); 37 | 38 | Bitmap personalPic = thaiSmartCard.getPersonalPicture(); 39 | 40 | if (personalPic == null) { 41 | Toast.makeText(getApplicationContext(), "Read Smart Card personal picture failed", Toast.LENGTH_LONG).show(); 42 | return; 43 | } 44 | 45 | // do something 46 | } 47 | 48 | @Override 49 | public void OnDetached(SmartCardDevice device) { 50 | Toast.makeText(getApplicationContext(), "Smart Card is removed", Toast.LENGTH_LONG).show(); 51 | } 52 | }); 53 | 54 | if (device == null) { 55 | Toast.makeText(getApplicationContext(), "Smart Card device not found", Toast.LENGTH_LONG).show(); 56 | } 57 | 58 | ``` 59 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.3.2' 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | 15 | 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Advanced-Logic/AndroidThaiNationalIDCard/941e97d3b9607fb4ab4e75d281c359763aa7d450/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Mar 14 18:35:41 ICT 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':thainationalidcard' 2 | -------------------------------------------------------------------------------- /thainationalidcard/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /thainationalidcard/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 28 5 | 6 | 7 | 8 | defaultConfig { 9 | minSdkVersion 21 10 | targetSdkVersion 28 11 | versionCode 1 12 | versionName "1.0.1" 13 | 14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 15 | 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | } 26 | 27 | dependencies { 28 | implementation fileTree(dir: 'libs', include: ['*.jar']) 29 | 30 | implementation 'com.android.support:appcompat-v7:28.0.0' 31 | testImplementation 'junit:junit:4.12' 32 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 33 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 34 | } 35 | -------------------------------------------------------------------------------- /thainationalidcard/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /thainationalidcard/src/androidTest/java/co/advancedlogic/thainationalidcard/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package co.advancedlogic.thainationalidcard; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("co.advancedlogic.thainationalidcard.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /thainationalidcard/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /thainationalidcard/src/main/java/co/advancedlogic/thainationalidcard/PreciseEscapeObject.java: -------------------------------------------------------------------------------- 1 | package co.advancedlogic.thainationalidcard; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.IOException; 7 | import java.util.LinkedList; 8 | 9 | public class PreciseEscapeObject { 10 | private static final String TAG = "PreciseEscapeObject"; 11 | 12 | private byte command; 13 | private byte[] data = null; 14 | private LinkedList elements = null; 15 | 16 | public PreciseEscapeObject(byte command) { 17 | this.command = command; 18 | this.elements = new LinkedList(); 19 | } 20 | 21 | public PreciseEscapeObject(byte command, byte[] data) { 22 | this.command = command; 23 | this.data = data; 24 | } 25 | 26 | public PreciseEscapeObject(byte command, PreciseEscapeObject element) { 27 | this.command = command; 28 | this.elements = new LinkedList(); 29 | this.elements.add(element); 30 | } 31 | 32 | public PreciseEscapeObject(byte command, LinkedList elements) { 33 | this.command = command; 34 | this.elements = elements; 35 | } 36 | 37 | public byte getCommand() { 38 | return this.command; 39 | } 40 | 41 | public byte[] getData() { 42 | return this.data; 43 | } 44 | 45 | public LinkedList getElements() { 46 | if (this.data != null) { 47 | return null; 48 | } 49 | return this.elements; 50 | } 51 | 52 | public void addElement(PreciseEscapeObject element) { 53 | if (this.data != null) { 54 | return; 55 | } 56 | this.elements.add(element); 57 | } 58 | 59 | public byte[] serialize() { 60 | byte[] data, tmp; 61 | int length, index; 62 | 63 | if (this.data != null) { 64 | tmp = this.data; 65 | } else if (this.elements.size() > 0) { 66 | ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); 67 | 68 | for (PreciseEscapeObject element : this.elements) { 69 | if ((tmp = element.serialize()) == null || tmp.length == 0) { 70 | Log.w(TAG, "PreciseEscapeObject serializing return failed"); 71 | try { 72 | dataStream.close(); 73 | } catch (IOException e) { 74 | e.printStackTrace(); 75 | } 76 | return null; 77 | } 78 | 79 | try { 80 | dataStream.write(tmp); 81 | } catch (IOException e) { 82 | Log.w(TAG, "PreciseEscapeObject serializing write to bytes buffer failed"); 83 | e.printStackTrace(); 84 | return null; 85 | } 86 | } 87 | tmp = dataStream.toByteArray(); 88 | 89 | try { 90 | dataStream.close(); 91 | } catch (IOException e) { 92 | e.printStackTrace(); 93 | } 94 | } else { 95 | Log.w(TAG, "PreciseEscapeObject serializing but data not found"); 96 | return null; 97 | } 98 | 99 | length = tmp.length; 100 | if (length < 0x80) { 101 | data = new byte[(length + 2)]; 102 | data[1] = (byte)length; 103 | index = 2; 104 | } else if (length <= 0xff) { 105 | data = new byte[(length + 3)]; 106 | data[1] = (byte)0x81; 107 | data[2] = (byte)length; 108 | index = 3; 109 | } else if (length <= 0xffff) { 110 | data = new byte[(length + 4)]; 111 | data[1] = (byte)0x82; 112 | data[2] = (byte)((length >> 8) & 0xff); 113 | data[3] = (byte)(length & 0xff); 114 | index = 4; 115 | } else if (length <= 0xffffff) { 116 | data = new byte[(length + 5)]; 117 | data[1] = (byte)0x83; 118 | data[2] = (byte)((length >> 16) & 0xff); 119 | data[3] = (byte)((length >> 8) & 0xff); 120 | data[4] = (byte)(length & 0xff); 121 | index = 5; 122 | } else { 123 | Log.w(TAG, "PreciseEscapeObject serializing data length exceed"); 124 | return null; 125 | } 126 | 127 | data[0] = this.command; 128 | System.arraycopy(tmp, 0, data, index, length); 129 | 130 | return data; 131 | } 132 | 133 | private static PreciseEscapeObject deserializePlainData(byte[] data) { 134 | byte[] buffer; 135 | int length; 136 | 137 | if (data == null) { 138 | Log.w(TAG, "Invalid data"); 139 | return null; 140 | } 141 | 142 | length = (int)data[1]; 143 | 144 | if (length < 0x80) { 145 | buffer = new byte[length]; 146 | if (length > 0) { 147 | System.arraycopy(data, 2, buffer, 0, length); 148 | } 149 | 150 | return new PreciseEscapeObject(data[0], buffer); 151 | } else if (length == 0x81) { 152 | length = (int)data[2]; 153 | buffer = new byte[length]; 154 | if (length > 0) { 155 | System.arraycopy(data, 3, buffer, 0, length); 156 | } 157 | 158 | return new PreciseEscapeObject(data[0], buffer); 159 | } else if (length == 0x82) { 160 | length = (((int)data[2]) << 8) | (int)data[3]; 161 | buffer = new byte[length]; 162 | if (length > 0) { 163 | System.arraycopy(data, 4, buffer, 0, length); 164 | } 165 | 166 | return new PreciseEscapeObject(data[0], buffer); 167 | } else if (length == 0x83) { 168 | length = (((int)data[2]) << 16) | (((int)data[3]) << 8) | (int)data[4]; 169 | buffer = new byte[length]; 170 | if (length > 0) { 171 | System.arraycopy(data, 5, buffer, 0, length); 172 | } 173 | 174 | return new PreciseEscapeObject(data[0], buffer); 175 | } else { 176 | Log.w(TAG, "Invalid data length"); 177 | return null; 178 | } 179 | } 180 | 181 | public static PreciseEscapeObject deserialize(byte[] data) { 182 | byte command; 183 | int length; 184 | 185 | if (data == null) { 186 | Log.w(TAG, "Invalid data"); 187 | return null; 188 | } 189 | 190 | command = data[0]; 191 | 192 | switch (command) { 193 | case (byte)0xc5: 194 | return PreciseEscapeObject.deserializePlainData(data); 195 | 196 | default: 197 | length = (int)data[1]; 198 | 199 | if (length < 0x80) { 200 | /// 201 | } else if (length == 0x81) { 202 | /// 203 | } else if (length == 0x82) { 204 | /// 205 | } else if (length == 0x83) { 206 | /// 207 | } else { 208 | Log.w(TAG, "Invalid data length"); 209 | return null; 210 | } 211 | } 212 | 213 | return null; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /thainationalidcard/src/main/java/co/advancedlogic/thainationalidcard/SmartCardDevice.java: -------------------------------------------------------------------------------- 1 | package co.advancedlogic.thainationalidcard; 2 | 3 | import android.app.PendingIntent; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.IntentFilter; 8 | import android.hardware.usb.UsbDevice; 9 | import android.hardware.usb.UsbDeviceConnection; 10 | import android.hardware.usb.UsbEndpoint; 11 | import android.hardware.usb.UsbInterface; 12 | import android.hardware.usb.UsbManager; 13 | import android.util.Log; 14 | 15 | import java.io.ByteArrayOutputStream; 16 | import java.io.IOException; 17 | import java.util.HashMap; 18 | import java.util.Set; 19 | 20 | public class SmartCardDevice { 21 | private static final String ACTION_USB_PERMISSION = "ninkoman.smartcardreader.USB_PERMISSION"; 22 | private static final String TAG = "SmartCardDevice"; 23 | 24 | private Context context; 25 | private UsbDevice device; 26 | private SmartCardMessage message; 27 | private PendingIntent mPermissionIntent; 28 | private boolean havePermission = false; 29 | private UsbDeviceConnection deviceConnection = null; 30 | private UsbInterface deviceInterface = null; 31 | private UsbEndpoint inputEndpoint = null; 32 | private UsbEndpoint outputEndpoint = null; 33 | 34 | private int infIndex = 0; 35 | private int endpointInputIndex = 0; 36 | private int endpointOutputIndex = 0; 37 | private boolean stopped = true; 38 | private boolean started = false; 39 | private boolean deviceDetachedRegister = false; 40 | 41 | private SmartCardDeviceEvent eventCallback = null; 42 | 43 | private BroadcastReceiver usbStateChangeReceiver = new BroadcastReceiver() { 44 | @Override 45 | public void onReceive(Context context, Intent intent) { 46 | Set keys = intent.getExtras().keySet(); 47 | 48 | Log.d(TAG, "Usb State change ==="); 49 | 50 | for (String key : keys) { 51 | Log.d(TAG, "key: " + key); 52 | } 53 | } 54 | }; 55 | 56 | public SmartCardDevice(Context context, UsbDevice device, int infIndex, int endpointInputIndex, int endpointOutputIndex, SmartCardDeviceEvent eventCallback) { 57 | if (context == null || device == null) { 58 | throw new NullPointerException(); 59 | } 60 | 61 | this.context = context; 62 | this.device = device; 63 | this.message = new SmartCardMessage(); 64 | 65 | this.infIndex = infIndex; 66 | this.endpointInputIndex = endpointInputIndex; 67 | this.endpointOutputIndex = endpointOutputIndex; 68 | this.eventCallback = eventCallback; 69 | 70 | mPermissionIntent = PendingIntent.getBroadcast(context, 0, new Intent(ACTION_USB_PERMISSION), 0); 71 | IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); 72 | context.registerReceiver(this.mUsbPermissionReceiver, filter); 73 | } 74 | 75 | @Override 76 | protected void finalize() throws Throwable { 77 | super.finalize(); 78 | this.stop(); 79 | } 80 | 81 | public interface SmartCardDeviceEvent { 82 | void OnReady(SmartCardDevice device); 83 | void OnDetached(SmartCardDevice device); 84 | } 85 | 86 | public static SmartCardDevice getSmartCardDevice(Context context, String nameContain, int infIndex, int endpointInputIndex, int endpointOutputIndex, SmartCardDeviceEvent eventCallback) { 87 | UsbManager manager; 88 | HashMap deviceList; 89 | UsbDevice device = null; 90 | SmartCardDevice cardDevice = null; 91 | 92 | manager = (UsbManager)context.getSystemService(Context.USB_SERVICE); 93 | if (manager == null) { 94 | Log.w(TAG, "USB manager not found"); 95 | return null; 96 | } 97 | 98 | deviceList = manager.getDeviceList(); 99 | if (deviceList == null) { 100 | Log.w(TAG, "USB device list not found"); 101 | return null; 102 | } 103 | 104 | for (String key : deviceList.keySet()) { 105 | Log.d(TAG, "Search device contain name [" + nameContain + "] with [" + deviceList.get(key).getProductName() + "] [" + deviceList.get(key).getDeviceName() + "]"); 106 | if (deviceList.get(key).getProductName().contains(nameContain)) { 107 | device = deviceList.get(key); 108 | Log.d(TAG, "Found device: " + device.getProductName()); 109 | break; 110 | } 111 | } 112 | 113 | if (device == null) { 114 | Log.w(TAG, "Device name [" + nameContain + "] not found"); 115 | return null; 116 | } 117 | 118 | cardDevice = new SmartCardDevice(context, device, infIndex, endpointInputIndex, endpointOutputIndex, eventCallback); 119 | 120 | cardDevice.start(); 121 | 122 | return cardDevice; 123 | } 124 | 125 | public static SmartCardDevice getSmartCardDevice(Context context, String nameContain, SmartCardDeviceEvent eventCallback) { 126 | UsbManager manager; 127 | HashMap deviceList; 128 | UsbDevice device = null; 129 | SmartCardDevice cardDevice = null; 130 | 131 | manager = (UsbManager)context.getSystemService(Context.USB_SERVICE); 132 | if (manager == null) { 133 | Log.w(TAG, "USB manager not found"); 134 | return null; 135 | } 136 | 137 | deviceList = manager.getDeviceList(); 138 | if (deviceList == null) { 139 | Log.w(TAG, "USB device list not found"); 140 | return null; 141 | } 142 | 143 | for (String key : deviceList.keySet()) { 144 | Log.d(TAG, "Search device contain name [" + nameContain + "] in [" + deviceList.get(key).getProductName() + "] [" + deviceList.get(key).getDeviceName() + "]"); 145 | Log.d(TAG, "Detail ==> [" + deviceList.get(key) +"]"); 146 | if (deviceList.get(key).getProductName().contains(nameContain)) { 147 | device = deviceList.get(key); 148 | Log.d(TAG, "Found device: " + device.getProductName()); 149 | break; 150 | } 151 | } 152 | 153 | if (device == null) { 154 | Log.w(TAG, "Device name [" + nameContain + "] not found"); 155 | return null; 156 | } 157 | 158 | UsbInterface intf = device.getInterface(0); 159 | int[] endpointIndex = new int[2]; 160 | int index = 0; 161 | 162 | for (int i = 0; i < intf.getEndpointCount(); i++) { 163 | if (intf.getEndpoint(i).getAttributes() == 2) { 164 | endpointIndex[index] = i; 165 | index++; 166 | 167 | if (index >= 2) { 168 | break; 169 | } 170 | } 171 | } 172 | if (index < 2) { 173 | Log.d(TAG, "Smart Card device endpoint detect failed"); 174 | return null; 175 | } 176 | 177 | cardDevice = new SmartCardDevice(context, device, 0, endpointIndex[0], endpointIndex[1], eventCallback); 178 | 179 | cardDevice.start(); 180 | 181 | return cardDevice; 182 | } 183 | 184 | public void start() { 185 | UsbManager manager = (UsbManager)this.context.getSystemService(Context.USB_SERVICE); 186 | if (this.started) { 187 | return; 188 | } 189 | this.started = true; 190 | 191 | if (manager != null) { 192 | Log.d(TAG, "Start request permission"); 193 | manager.requestPermission(device, mPermissionIntent); 194 | } else { 195 | throw new RuntimeException("USB manager not found"); 196 | } 197 | } 198 | 199 | public void stop() { 200 | if (!this.stopped) { 201 | //this.deviceConnection.close(); 202 | this.stopped = true; 203 | } 204 | this.started = false; 205 | 206 | this.context.unregisterReceiver(this.usbStateChangeReceiver); 207 | 208 | this.havePermission = false; 209 | } 210 | 211 | public String getDeviceProductName() { 212 | if (this.device != null) { 213 | return this.device.getProductName(); 214 | } 215 | 216 | return ""; 217 | } 218 | 219 | public boolean reset() { 220 | SmartCardMessage.DataBlock dataBlock; 221 | byte[] data; 222 | byte[] message = this.message.getMessageSlotReset(); 223 | 224 | if (this.havePermission) { 225 | if (!this.prepareConnection()) { 226 | Log.w(TAG, "prepareConnection() failed"); 227 | return false; 228 | } 229 | 230 | if (!this.sendRequestMessage(message)) { 231 | Log.w(TAG, "sendRequestMessage() error"); 232 | return false; 233 | } 234 | 235 | try { 236 | if ((data = this.receiveResponseMessage()) == null) { 237 | Log.w(TAG, "receiveResponseMessage() error"); 238 | return false; 239 | } 240 | } catch (IOException e) { 241 | Log.w(TAG, "receiveResponseMessage() failed"); 242 | return false; 243 | } 244 | 245 | dataBlock = this.message.parseDataBlock(data); 246 | if (dataBlock != null && dataBlock.data != null) { 247 | StringBuilder sb = new StringBuilder(); 248 | 249 | for (byte b: dataBlock.data) { 250 | sb.append(String.format("%02x", b)); 251 | } 252 | 253 | Log.d(TAG, "Card reset response data[" + sb.toString() + "]"); 254 | 255 | if (dataBlock.status != 0 || dataBlock.error != 0) { 256 | Log.w(TAG, "Card reset return abnormal status " + dataBlock.status + ":" + dataBlock.error); 257 | return false; 258 | } 259 | 260 | if (dataBlock.data[0] != (byte)0x82) { 261 | Log.w(TAG, String.format("Card reset return abnormal code 0x%02x", dataBlock.data[0])); 262 | return false; 263 | } 264 | 265 | return true; 266 | } else { 267 | Log.w(TAG, "Card reset return fail"); 268 | return false; 269 | } 270 | } 271 | 272 | return false; 273 | } 274 | 275 | public boolean isStarted() { 276 | return !this.started; 277 | } 278 | 279 | //public void setMessageTypeT1(boolean isT1) { 280 | // this.message.setMessageTypeT1(isT1); 281 | //} 282 | 283 | private final BroadcastReceiver mUsbPermissionReceiver = new BroadcastReceiver() { 284 | @Override 285 | public void onReceive(Context context, Intent intent) { 286 | String action = intent.getAction(); 287 | UsbManager manager; 288 | 289 | if (ACTION_USB_PERMISSION.equals(action)) { 290 | Log.d(TAG, "USB permission broadcast received"); 291 | synchronized (this) { 292 | UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); 293 | 294 | if (device != null && device.getDeviceName().equals(SmartCardDevice.this.device.getDeviceName())) { 295 | if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { 296 | manager = (UsbManager)context.getSystemService(Context.USB_SERVICE); 297 | if (manager == null) { 298 | throw new RuntimeException("USB manager not found"); 299 | } 300 | 301 | SmartCardDevice.this.deviceConnection = manager.openDevice(device); 302 | 303 | if (SmartCardDevice.this.deviceConnection == null) { 304 | throw new RuntimeException("Invalid USB device connection"); 305 | } 306 | 307 | SmartCardDevice.this.deviceInterface = device.getInterface(infIndex); 308 | SmartCardDevice.this.inputEndpoint = SmartCardDevice.this.deviceInterface.getEndpoint(endpointInputIndex); 309 | SmartCardDevice.this.outputEndpoint = SmartCardDevice.this.deviceInterface.getEndpoint(endpointOutputIndex); 310 | 311 | if (SmartCardDevice.this.deviceInterface == null || SmartCardDevice.this.inputEndpoint == null || SmartCardDevice.this.outputEndpoint == null) { 312 | throw new RuntimeException("Invalid USB device interface or endpoint"); 313 | } 314 | 315 | SmartCardDevice.this.havePermission = true; 316 | SmartCardDevice.this.stopped = false; 317 | 318 | if (SmartCardDevice.this.eventCallback != null) 319 | SmartCardDevice.this.eventCallback.OnReady(SmartCardDevice.this); 320 | Log.d(TAG, "Card device is ready"); 321 | 322 | if (!SmartCardDevice.this.deviceDetachedRegister) { 323 | IntentFilter filter = new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED); 324 | SmartCardDevice.this.context.registerReceiver(SmartCardDevice.this.mUsbDetachedReceiver, filter); 325 | SmartCardDevice.this.deviceDetachedRegister = true; 326 | } 327 | 328 | SmartCardDevice.this.context.unregisterReceiver(SmartCardDevice.this.mUsbPermissionReceiver); 329 | 330 | IntentFilter filter = new IntentFilter(); 331 | filter.addAction("android.hardware.usb.action.USB_STATE"); 332 | context.registerReceiver(SmartCardDevice.this.usbStateChangeReceiver, filter); 333 | } else { 334 | SmartCardDevice.this.havePermission = false; 335 | throw new RuntimeException("Device is not granted"); 336 | } 337 | 338 | 339 | } 340 | } 341 | } 342 | } 343 | }; 344 | 345 | private final BroadcastReceiver mUsbDetachedReceiver = new BroadcastReceiver() { 346 | @Override 347 | public void onReceive(Context context, Intent intent) { 348 | String action = intent.getAction(); 349 | if (action != null && action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { 350 | UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); 351 | 352 | Log.d(TAG, "USB device is detached"); 353 | 354 | if (device.getDeviceName().equals(SmartCardDevice.this.device.getDeviceName())) { 355 | if (SmartCardDevice.this.eventCallback != null) 356 | SmartCardDevice.this.eventCallback.OnDetached(SmartCardDevice.this); 357 | SmartCardDevice.this.stop(); 358 | 359 | SmartCardDevice.this.context.unregisterReceiver(SmartCardDevice.this.mUsbDetachedReceiver); 360 | SmartCardDevice.this.deviceDetachedRegister = false; 361 | } 362 | } 363 | } 364 | }; 365 | 366 | public SmartCardMessage.DataBlock getATR() { 367 | if (!this.havePermission) { 368 | Log.w(TAG, "USB permission require, please call start() first"); 369 | return null; 370 | } 371 | 372 | byte[] message = this.message.getMessageIccPowerOn(); 373 | 374 | if (!this.prepareConnection()) { 375 | Log.w(TAG, "prepareConnection() failed"); 376 | return null; 377 | } 378 | 379 | if (!this.sendRequestMessage(message)) { 380 | Log.w(TAG, "sendRequestMessage() failed"); 381 | return null; 382 | } 383 | 384 | try { 385 | if ((message = this.receiveResponseMessage()) == null) { 386 | Log.w(TAG, "receiveResponseMessage() error"); 387 | return null; 388 | } 389 | } catch (IOException e) { 390 | Log.w(TAG, "receiveResponseMessage() failed"); 391 | return null; 392 | } 393 | 394 | return this.message.parseDataBlock(message); 395 | } 396 | 397 | public SmartCardMessage.DataBlock sendAPDU(byte[] dataAPDU) { 398 | byte[] data; 399 | 400 | if (!this.havePermission) { 401 | Log.w(TAG, "USB permission require, please call start() first"); 402 | return null; 403 | } 404 | 405 | byte[][] messages = this.message.getMessageXfrBlock(dataAPDU); 406 | 407 | if (messages == null) { 408 | Log.w(TAG, "getMessageXfrBlock() return failed"); 409 | return null; 410 | } 411 | 412 | if (!this.prepareConnection()) { 413 | Log.w(TAG, "prepareConnection() failed"); 414 | return null; 415 | } 416 | 417 | for (byte[] message: messages) { 418 | if (!this.sendRequestMessage(message)) { 419 | Log.w(TAG, "sendRequestMessage() error"); 420 | return null; 421 | } 422 | } 423 | 424 | try { 425 | if ((data = this.receiveResponseMessage()) == null) { 426 | Log.w(TAG, "receiveResponseMessage() failed"); 427 | return null; 428 | } 429 | } catch (IOException e) { 430 | Log.w(TAG, "receiveResponseMessage() error"); 431 | return null; 432 | } 433 | 434 | SmartCardMessage.DataBlock dataBlock = this.message.parseDataBlock(data); 435 | if (dataBlock != null && dataBlock.data != null) { 436 | StringBuilder sb = new StringBuilder(); 437 | 438 | for (byte b: dataBlock.data) { 439 | sb.append(String.format("%02x", b)); 440 | } 441 | 442 | Log.d(TAG, "Response data[" + sb.toString() + "]"); 443 | } 444 | 445 | return dataBlock; 446 | } 447 | 448 | public SmartCardMessage.EscapeResponseBlock sendEscapeCommand(byte[] dataEscape) { 449 | byte[] data; 450 | 451 | if (!this.havePermission) { 452 | Log.w(TAG, "USB permission require, please call start() first"); 453 | return null; 454 | } 455 | 456 | byte[] message = this.message.getMessageEscapeRequest(dataEscape); 457 | 458 | if (message == null) { 459 | Log.w(TAG, "getMessageEscapeRequest() return failed"); 460 | return null; 461 | } 462 | 463 | if (!this.prepareConnection()) { 464 | Log.w(TAG, "prepareConnection() failed"); 465 | return null; 466 | } 467 | 468 | if (!this.sendRequestMessage(message)) { 469 | Log.w(TAG, "sendRequestMessage() error"); 470 | return null; 471 | } 472 | 473 | try { 474 | if ((data = this.receiveResponseMessage()) == null) { 475 | Log.w(TAG, "receiveResponseMessage() failed"); 476 | return null; 477 | } 478 | } catch (IOException e) { 479 | Log.w(TAG, "receiveResponseMessage() error"); 480 | return null; 481 | } 482 | 483 | SmartCardMessage.EscapeResponseBlock response = this.message.parseEscapeResponseBlock(data); 484 | 485 | if (response != null && response.data != null) { 486 | StringBuilder sb = new StringBuilder(); 487 | 488 | for (byte b: response.data) { 489 | sb.append(String.format("%02x", b)); 490 | } 491 | 492 | Log.d(TAG, "Response escape data [" + sb.toString() + "]"); 493 | } 494 | 495 | return response; 496 | } 497 | 498 | private boolean prepareConnection() { 499 | if (!this.havePermission) { 500 | Log.w(TAG, "USB permission require, please call start() first"); 501 | return false; 502 | } 503 | 504 | this.deviceConnection.claimInterface(this.deviceInterface, true); 505 | 506 | return true; 507 | } 508 | 509 | private boolean sendRequestMessage(byte[] message) { 510 | int length; 511 | 512 | if (!this.havePermission) { 513 | Log.w(TAG, "USB permission require, please call start() first"); 514 | return false; 515 | } 516 | 517 | if (message == null || message.length == 0) { 518 | Log.w(TAG, "message is null or invalid length"); 519 | return false; 520 | } 521 | 522 | StringBuilder sb = new StringBuilder(); 523 | for (byte b: message) { 524 | sb.append(String.format("%02x", (byte)b)); 525 | } 526 | Log.d(TAG, "Sending message [" + message.length + "][" + sb.toString() + "]"); 527 | 528 | length = this.deviceConnection.bulkTransfer(this.inputEndpoint, message, message.length, 0); 529 | 530 | if (length <= 0 || length != message.length) { 531 | Log.w(TAG, "message sending return invalid length " + length + "/" + message.length); 532 | return false; 533 | } 534 | 535 | return true; 536 | } 537 | 538 | private byte[] receiveResponseMessage() throws IOException { 539 | int length, totalLength; 540 | byte[] buffer; 541 | ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); 542 | 543 | if (!this.havePermission) { 544 | Log.w(TAG, "USB permission require, please call start() first"); 545 | return null; 546 | } 547 | 548 | buffer = new byte[this.outputEndpoint.getMaxPacketSize()]; 549 | 550 | length = this.deviceConnection.bulkTransfer(this.outputEndpoint, buffer, buffer.length, 0); 551 | 552 | if (length < 10) { 553 | Log.w(TAG, "receive message invalid length " + length); 554 | return null; 555 | } 556 | 557 | dataStream = new ByteArrayOutputStream(); 558 | dataStream.write(buffer, 0, length); 559 | 560 | totalLength = (((int)buffer[4] & 0xff) << 24) | (((int)buffer[3] & 0xff) << 16) | (((int)buffer[2] & 0xff) << 8) | ((int)buffer[1] & 0xff); 561 | totalLength += 10; 562 | 563 | while (dataStream.size() < totalLength) { 564 | length = this.deviceConnection.bulkTransfer(this.outputEndpoint, buffer, buffer.length, 0); 565 | if (length < 0) { 566 | Log.w(TAG, "receive continues message return error"); 567 | dataStream.close(); 568 | return null; 569 | } else if (length == 0) { 570 | if (dataStream.size() < totalLength) { 571 | Log.w(TAG, "receive continues message not success"); 572 | dataStream.close(); 573 | return null; 574 | } else { 575 | break; 576 | } 577 | } 578 | 579 | dataStream.write(buffer, 0, length); 580 | } 581 | 582 | buffer = dataStream.toByteArray(); 583 | dataStream.close(); 584 | 585 | return buffer; 586 | } 587 | } 588 | -------------------------------------------------------------------------------- /thainationalidcard/src/main/java/co/advancedlogic/thainationalidcard/SmartCardMessage.java: -------------------------------------------------------------------------------- 1 | package co.advancedlogic.thainationalidcard; 2 | 3 | import android.util.Log; 4 | 5 | public final class SmartCardMessage { 6 | private static final String TAG = "SmartCardMessage"; 7 | 8 | private boolean isT1 = false; 9 | private int slotNumber = 0; 10 | private int sequence = 0x00; 11 | private int timeout = 0; 12 | private int exchangeLevel = 0; 13 | private int maxPacketSize = 64; 14 | 15 | public SmartCardMessage() { 16 | } 17 | 18 | public SmartCardMessage(boolean t1) { 19 | this.isT1 = t1; 20 | } 21 | 22 | public void setMessageTypeT1(boolean t1) { 23 | this.isT1 = t1; 24 | } 25 | 26 | public boolean setSlotNumber(int slotNumber) { 27 | if (slotNumber >= 0 && slotNumber <= 127) { 28 | this.slotNumber = slotNumber; 29 | return true; 30 | } 31 | 32 | Log.w(TAG, "Invalid slot number range"); 33 | return false; 34 | } 35 | 36 | public void setMaxPacketSize(int size) { 37 | this.maxPacketSize = size; 38 | } 39 | 40 | private void initProperties(byte[] message, int type) { 41 | if (message == null) { 42 | Log.w(TAG, "message is null"); 43 | return; 44 | } 45 | 46 | message[0] = (byte)type; 47 | message[5] = (byte)this.slotNumber; 48 | message[6] = (byte)this.sequence; 49 | message[7] = (byte)this.timeout; 50 | message[8] = (byte)this.exchangeLevel; 51 | message[9] = (byte)0x00; 52 | 53 | this.sequence++; 54 | } 55 | 56 | private void initMessageLength(byte[] message, int length) { 57 | if (message == null) { 58 | Log.w(TAG, "message is null"); 59 | return; 60 | } 61 | 62 | message[1] = (byte)(length & 0xff); 63 | message[2] = (byte)((length >> 8) & 0xff); 64 | message[3] = (byte)((length >> 16) & 0xff); 65 | message[4] = (byte)((length >> 24) & 0xff); 66 | } 67 | 68 | public byte[] getMessageIccPowerOn() { 69 | byte[] message = new byte[10]; 70 | 71 | this.initProperties(message,0x62); 72 | 73 | return message; 74 | } 75 | 76 | public byte[] getMessageIccPowerOff() { 77 | byte[] message = new byte[10]; 78 | 79 | this.initProperties(message,0x63); 80 | 81 | return message; 82 | } 83 | 84 | public byte[] getMessageGetSlotStatus() { 85 | byte[] message = new byte[10]; 86 | 87 | this.initProperties(message,0x65); 88 | 89 | return message; 90 | } 91 | 92 | public byte[] getMessageSlotReset() { 93 | byte[] message = new byte[10]; 94 | 95 | this.initProperties(message,0x6d); 96 | 97 | return message; 98 | } 99 | 100 | public byte[][] getMessageXfrBlock(byte[] data) { 101 | byte[][] messages; 102 | byte[] block, dataTmp; 103 | int blockSize; 104 | byte xorSum = 0; 105 | 106 | if (this.isT1) { 107 | if (data.length > 254) { 108 | Log.w(TAG, "Invalid data block length"); 109 | return null; 110 | } 111 | dataTmp = new byte[data.length + 4]; 112 | dataTmp[2] = (byte)data.length; 113 | System.arraycopy(data, 0, dataTmp, 3, data.length); 114 | for (byte b: dataTmp) { 115 | xorSum ^= b; 116 | } 117 | dataTmp[dataTmp.length - 1] = xorSum; 118 | 119 | data = dataTmp; 120 | 121 | StringBuilder sb = new StringBuilder(); 122 | for (byte b: data) { 123 | sb.append(String.format("%02x", (byte)b)); 124 | } 125 | Log.d(TAG, "APDU T=1 [" + data.length + "][" + sb.toString() + "]"); 126 | } else { 127 | StringBuilder sb = new StringBuilder(); 128 | for (byte b: data) { 129 | sb.append(String.format("%02x", (byte)b)); 130 | } 131 | Log.d(TAG, "APDU T=0 [" + data.length + "][" + sb.toString() + "]"); 132 | } 133 | 134 | int blockCount = ((data.length + 10) / this.maxPacketSize) + ((((data.length + 10) % this.maxPacketSize) > 0) ? 1:0); 135 | 136 | messages = new byte[blockCount][]; 137 | 138 | for (int i = 0; i < blockCount; i++) { 139 | blockSize = (((data.length + 10) - (i * this.maxPacketSize)) > this.maxPacketSize) ? this.maxPacketSize:((data.length + 10) - (i * this.maxPacketSize)); 140 | block = new byte[blockSize]; 141 | 142 | if (i == 0) { 143 | this.initProperties(block, 0x6f); 144 | this.initMessageLength(block, data.length); 145 | blockSize = ((data.length + 10) > this.maxPacketSize) ? (this.maxPacketSize - 10):data.length; 146 | 147 | System.arraycopy(data, 0, block, 10, blockSize); 148 | } else { 149 | blockSize = (((data.length + 10) - (i * this.maxPacketSize)) > this.maxPacketSize) ? this.maxPacketSize:((data.length + 10) - (i * this.maxPacketSize)); 150 | 151 | System.arraycopy(data, (i * this.maxPacketSize) - 10, block, 0, blockSize); 152 | } 153 | 154 | messages[i] = block; 155 | } 156 | 157 | return messages; 158 | } 159 | 160 | public byte[] getMessageEscapeRequest(byte[] data) { 161 | byte[] messages; 162 | 163 | messages = new byte[data.length + 10]; 164 | 165 | this.initProperties(messages, 0x6b); 166 | this.initMessageLength(messages, data.length); 167 | 168 | System.arraycopy(data, 0, messages, 10, data.length); 169 | 170 | return messages; 171 | } 172 | 173 | public DataBlock parseDataBlock(byte[] data) { 174 | int length; 175 | DataBlock dataBlock; 176 | 177 | if (data == null) { 178 | Log.w(TAG, "data is null"); 179 | return null; 180 | } 181 | 182 | Log.d(TAG, "Parsing data length " + data.length); 183 | 184 | if (data[0] != (byte)0x80) { 185 | Log.w(TAG, String.format("Invalid data type [%02x]", (byte)data[0])); 186 | return null; 187 | } 188 | 189 | dataBlock = new DataBlock(); 190 | 191 | length = (int)((((int)data[4] & 0xff) << 24) | (((int)data[3] & 0xff) << 16 ) | (((int)data[2] & 0xff) << 8) | ((int)data[1] & 0xff)); 192 | 193 | if (data.length < length + 10) { 194 | Log.w(TAG, String.format("Invalid data length %d/%d", data.length, length + 10)); 195 | return null; 196 | } 197 | 198 | dataBlock.status = (int)data[7]; 199 | dataBlock.error = (int)data[8]; 200 | 201 | if (length > 0) { 202 | if (data[10] == (byte)0x3b) { 203 | dataBlock.dataType = DataType.ATR; 204 | } else if (data[10] == (byte)0x61 || data[10] == (byte)0x6c) { 205 | dataBlock.dataType = DataType.APDU_HEADER; 206 | } else { 207 | dataBlock.dataType = DataType.APDU_DATA; 208 | } 209 | } 210 | 211 | dataBlock.data = new byte[length]; 212 | 213 | System.arraycopy(data, 10, dataBlock.data, 0, length); 214 | 215 | return dataBlock; 216 | } 217 | 218 | public EscapeResponseBlock parseEscapeResponseBlock(byte[] data) { 219 | int length; 220 | EscapeResponseBlock escapeBlock; 221 | 222 | if (data == null) { 223 | Log.w(TAG, "escape is null"); 224 | return null; 225 | } 226 | 227 | Log.d(TAG, "Parsing escape length " + data.length); 228 | 229 | if (data[0] != (byte)0x83) { 230 | Log.w(TAG, String.format("Invalid escape type [%02x]", (byte)data[0])); 231 | return null; 232 | } 233 | 234 | length = (int)((((int)data[4] & 0xff) << 24) | (((int)data[3] & 0xff) << 16 ) | (((int)data[2] & 0xff) << 8) | ((int)data[1] & 0xff)); 235 | 236 | escapeBlock = new EscapeResponseBlock(); 237 | 238 | escapeBlock.status = data[7]; 239 | escapeBlock.error = data[8]; 240 | 241 | escapeBlock.data = new byte[length]; 242 | if (length > 0) { 243 | System.arraycopy(data, 10, escapeBlock.data, 0, length); 244 | } 245 | 246 | return escapeBlock; 247 | } 248 | 249 | public enum DataType { 250 | UNKNOWN, 251 | ATR, 252 | APDU_HEADER, 253 | APDU_DATA 254 | } 255 | 256 | public class DataBlock { 257 | public DataType dataType = DataType.UNKNOWN; 258 | public byte[] data = null; 259 | public int status = 0; 260 | public int error = 0; 261 | } 262 | 263 | public class EscapeResponseBlock { 264 | public byte[] data = null; 265 | public int status = 0; 266 | public int error = 0; 267 | public int rfu; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /thainationalidcard/src/main/java/co/advancedlogic/thainationalidcard/ThaiSmartCard.java: -------------------------------------------------------------------------------- 1 | package co.advancedlogic.thainationalidcard; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.BitmapFactory; 5 | import android.util.Log; 6 | 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.UnsupportedEncodingException; 9 | import java.security.InvalidAlgorithmParameterException; 10 | import java.security.InvalidKeyException; 11 | import java.security.MessageDigest; 12 | import java.security.NoSuchAlgorithmException; 13 | import java.util.Arrays; 14 | import java.util.Locale; 15 | 16 | import javax.crypto.BadPaddingException; 17 | import javax.crypto.Cipher; 18 | import javax.crypto.IllegalBlockSizeException; 19 | import javax.crypto.NoSuchPaddingException; 20 | import javax.crypto.SecretKey; 21 | import javax.crypto.spec.IvParameterSpec; 22 | import javax.crypto.spec.SecretKeySpec; 23 | 24 | public final class ThaiSmartCard { 25 | private static final String TAG = "ThaiSmartCard"; 26 | 27 | private SmartCardDevice device; 28 | 29 | public ThaiSmartCard(SmartCardDevice device) { 30 | if (device == null) { 31 | throw new NullPointerException("Invalid Device"); 32 | } 33 | 34 | if (!device.isStarted()) { 35 | device.start(); 36 | } 37 | 38 | this.device = device; 39 | } 40 | 41 | public boolean isInserted() { 42 | SmartCardMessage.DataBlock data; 43 | return ((data = this.device.getATR()) != null && data.dataType == SmartCardMessage.DataType.ATR && data.status == 0 && data.error == 0); 44 | } 45 | 46 | private SmartCardMessage.DataBlock getCardData(byte[] requestMessage) { 47 | SmartCardMessage.DataBlock data; 48 | byte[] dataRequestMessage = new byte[5]; 49 | 50 | if (requestMessage == null) { 51 | Log.d(TAG, "Invalid request message"); 52 | return null; 53 | } 54 | 55 | if ((data = this.device.sendAPDU(requestMessage)) == null) { 56 | Log.w(TAG, "APDU header request fail"); 57 | return null; 58 | } 59 | 60 | if (data.status != 0 || data.error != 0) { 61 | Log.w(TAG, "APDU header response abnormal [" + data.status+ ":" + data.error + "]"); 62 | return null; 63 | } 64 | 65 | if (data.data.length != 2) { 66 | Log.w(TAG, "APDU header response invalid length: " + data.data.length); 67 | return null; 68 | } 69 | 70 | if (data.data[0] == (byte)0x61) { 71 | dataRequestMessage[0] = (byte)0x00; 72 | dataRequestMessage[1] = (byte)0xc0; 73 | dataRequestMessage[2] = (byte)0x00; 74 | dataRequestMessage[3] = (byte)0x00; 75 | } else if (data.data[0] == (byte)0x6c) { 76 | System.arraycopy(requestMessage, 0, dataRequestMessage, 0, 4); 77 | } else { 78 | Log.w(TAG, String.format("APDU header response invalid code: %02x", (byte)data.data[0])); 79 | return null; 80 | } 81 | 82 | dataRequestMessage[4] = data.data[1]; 83 | 84 | if ((data = this.device.sendAPDU(dataRequestMessage)) == null) { 85 | Log.w(TAG, "APDU body request fail"); 86 | return null; 87 | } 88 | 89 | return data; 90 | } 91 | 92 | private boolean selectAppletChipData() { 93 | byte[] message = new byte[]{(byte)0x00, (byte)0xa4, (byte)0x04, (byte)0x00}; 94 | SmartCardMessage.DataBlock data; 95 | 96 | return ((data = this.getCardData(message)) != null && data.status == 0 && data.error == 0); 97 | } 98 | 99 | private boolean selectAppletStorageData() { 100 | byte[] message = new byte[]{(byte)0x00, (byte)0xa4, (byte)0x04, (byte)0x00, (byte)0x08, (byte)0xa0, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x54, (byte)0x48, (byte)0x00, (byte)0x01}; 101 | SmartCardMessage.DataBlock data; 102 | 103 | return ((data = this.getCardData(message)) != null && data.status == 0 && data.error == 0); 104 | } 105 | 106 | private boolean selectAppletExtension() { 107 | byte[] message = new byte[]{(byte)0x00, (byte)0xa4, (byte)0x04, (byte)0x00, (byte)0x08, (byte)0xa0, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x84, (byte)0x06, (byte)0x00, (byte)0x02}; 108 | SmartCardMessage.DataBlock data; 109 | 110 | return ((data = this.getCardData(message)) != null && data.status == 0 && data.error == 0); 111 | } 112 | 113 | private boolean selectAppletBio() { 114 | byte[] message = new byte[]{(byte)0x00, (byte)0xa4, (byte)0x04, (byte)0x00, (byte)0x08, (byte)0xa0, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x84, (byte)0x06, (byte)0x00, (byte)0x00}; 115 | SmartCardMessage.DataBlock data; 116 | 117 | return ((data = this.getCardData(message)) != null && data.status == 0 && data.error == 0); 118 | } 119 | 120 | private String byteArrayToHexString(byte[] input) { 121 | StringBuilder output; 122 | 123 | if (input == null) { 124 | return ""; 125 | } 126 | 127 | output = new StringBuilder(); 128 | 129 | for (byte b: input) { 130 | output.append(String.format("%02x", b)); 131 | } 132 | 133 | return output.toString(); 134 | } 135 | 136 | private String byteArrayToHexString(byte[] input, int index, int length) { 137 | byte[] selectBytes; 138 | 139 | if ((length + index) > input.length) { 140 | length = input.length - index; 141 | } 142 | 143 | selectBytes = new byte[length]; 144 | 145 | System.arraycopy(input, index, selectBytes, 0, length); 146 | 147 | return this.byteArrayToHexString(selectBytes); 148 | } 149 | 150 | private byte[] hexStringToByteArray(String hex) { 151 | int len = hex.length(); 152 | byte[] data = new byte[len / 2]; 153 | for (int i = 0; i < len; i += 2) { 154 | data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i+1), 16)); 155 | } 156 | 157 | return data; 158 | } 159 | 160 | public String getCardID() { 161 | SmartCardMessage.DataBlock data; 162 | 163 | if (!this.selectAppletChipData()) { 164 | Log.d(TAG, "selectAppletChipData fail"); 165 | return null; 166 | } 167 | 168 | byte[] requestMessage = new byte[]{(byte)0x80, (byte)0xca, (byte)0x9f, (byte)0x7f}; 169 | 170 | if ((data = this.getCardData(requestMessage)) == null) { 171 | Log.d(TAG, "Get chip card information fail"); 172 | return null; 173 | } 174 | 175 | if (data.status != 0 || data.error != 0 || data.data.length != 45 + 2) { 176 | Log.w(TAG, String.format("Invalid chip card information [%d][%d][%d][%s]", data.status, data.error, data.data.length, this.byteArrayToHexString(data.data))); 177 | return null; 178 | } 179 | 180 | return this.byteArrayToHexString(data.data, 13, 8); 181 | } 182 | 183 | public class PersonalInformation { 184 | public String CardInfo; 185 | public String PersonalID; 186 | public String NameTH; 187 | public String NameEN; 188 | public String BirthDate; 189 | public String Address; 190 | public String PictureTag; 191 | public String Issuer; 192 | public String IssuerCode; 193 | public String IssueDate; 194 | public String ExpireDate; 195 | } 196 | 197 | public PersonalInformation getPersonalInformation() { 198 | SmartCardMessage.DataBlock data; 199 | PersonalInformation personalInformation; 200 | 201 | byte[] block1_1 = new byte[]{(byte)0x80, (byte)0xb0, (byte)0x00, (byte)0x00, (byte)0x02, (byte)0x00, (byte)0xff}; 202 | byte[] block1_2 = new byte[]{(byte)0x80, (byte)0xb0, (byte)0x00, (byte)0xff, (byte)0x02, (byte)0x00, (byte)0x7a}; 203 | byte[] block2 = new byte[]{(byte)0x80, (byte)0xb0, (byte)0x15, (byte)0x79, (byte)0x02, (byte)0x00, (byte)0xae}; 204 | 205 | if (!this.selectAppletStorageData()) { 206 | Log.d(TAG, "selectAppletStorageData fail"); 207 | return null; 208 | } 209 | 210 | byte[] buffer = new byte[377]; 211 | 212 | if ((data = this.getCardData(block1_1)) == null) { 213 | Log.d(TAG, "Get personal information block 1-1 failed"); 214 | return null; 215 | } 216 | 217 | if (data.status != 0 || data.error != 0 || data.data.length != 0xff + 2) { 218 | Log.w(TAG, String.format("Invalid personal information block 1-1 [%d][%d][%d][%s]", data.status, data.error, data.data.length, this.byteArrayToHexString(data.data))); 219 | return null; 220 | } 221 | 222 | System.arraycopy(data.data, 0, buffer,0, 0xff); 223 | 224 | if ((data = this.getCardData(block1_2)) == null) { 225 | Log.w(TAG, "Get personal information block 1-2 failed"); 226 | return null; 227 | } 228 | 229 | if (data.status != 0 || data.error != 0 || data.data.length != 0x7a + 2) { 230 | Log.w(TAG, String.format("Invalid personal information block 1-2 [%d][%d][%d][%s]", data.status, data.error, data.data.length, this.byteArrayToHexString(data.data))); 231 | return null; 232 | } 233 | 234 | System.arraycopy(data.data, 0, buffer,0xff, 0x7a); 235 | 236 | if ((data = this.getCardData(block2)) == null) { 237 | Log.w(TAG, "Get personal information block 2 failed"); 238 | return null; 239 | } 240 | 241 | if (data.status != 0 || data.error != 0 || data.data.length != 0xae + 2) { 242 | Log.w(TAG, String.format("Invalid personal information block 2 [%d][%d][%d][%s]", data.status, data.error, data.data.length, this.byteArrayToHexString(data.data))); 243 | return null; 244 | } 245 | 246 | // NameTH split 247 | byte[] nameTHBuffer; 248 | int length; 249 | 250 | nameTHBuffer = Arrays.copyOfRange(buffer, 17, 117); 251 | for (length = 100; length > 0 && nameTHBuffer[length - 1] == 0x20; length--); 252 | 253 | for (int i = 0; i < length; i++) { 254 | if (nameTHBuffer[i] == 0x23) { 255 | nameTHBuffer[i] = 0x20; 256 | } 257 | } 258 | nameTHBuffer = Arrays.copyOf(nameTHBuffer, length); 259 | 260 | // NameTH split 261 | byte[] nameENBuffer; 262 | 263 | nameENBuffer = Arrays.copyOfRange(buffer, 117, 217); 264 | for (length = 100; length > 0 && nameENBuffer[length - 1] == 0x20; length--); 265 | 266 | for (int i = 0; i < length; i++) { 267 | if (nameENBuffer[i] == 0x23) { 268 | nameENBuffer[i] = 0x20; 269 | } 270 | } 271 | nameENBuffer = Arrays.copyOf(nameENBuffer, length); 272 | 273 | // issuer split 274 | byte[] issuerBuffer; 275 | int index; 276 | 277 | for (index = 246; index < 346; index++) { 278 | if (buffer[index] == 0x20) { 279 | break; 280 | } 281 | } 282 | if (index >= 346) { 283 | Log.w(TAG, "Invalid issuer data split"); 284 | return null; 285 | } 286 | 287 | issuerBuffer = Arrays.copyOfRange(buffer, 246, index); 288 | 289 | // Address split 290 | byte[] addressBuffer; 291 | 292 | addressBuffer = Arrays.copyOfRange(data.data, 0, 160); 293 | for (length = 160; length > 0 && addressBuffer[length - 1] == 0x20; length--); 294 | 295 | for (int i = 0; i < length; i++) { 296 | if (addressBuffer[i] == 0x23) { 297 | addressBuffer[i] = 0x20; 298 | } 299 | } 300 | addressBuffer = Arrays.copyOf(addressBuffer, length); 301 | 302 | // return object 303 | personalInformation = new PersonalInformation(); 304 | 305 | try { 306 | personalInformation.CardInfo = String.format("%s-%s-%s-%s", new String(Arrays.copyOfRange(buffer, 375, 377), "TIS620"), new String(Arrays.copyOfRange(buffer, 0, 4), "TIS620"), new String(Arrays.copyOfRange(buffer, 226, 237), "TIS620"), new String(Arrays.copyOfRange(buffer, 238, 246), "TIS620")); 307 | personalInformation.PersonalID = new String(Arrays.copyOfRange(buffer, 4, 17), "TIS620"); 308 | personalInformation.NameTH = new String(nameTHBuffer, "TIS620"); 309 | personalInformation.NameEN = new String(nameENBuffer, "TIS620"); 310 | personalInformation.BirthDate = new String(Arrays.copyOfRange(buffer, 217, 225), "TIS620"); 311 | personalInformation.Issuer = new String(issuerBuffer, "TIS620"); 312 | personalInformation.IssuerCode = new String(Arrays.copyOfRange(buffer, 346, 359), "TIS620"); 313 | personalInformation.IssueDate = new String(Arrays.copyOfRange(buffer, 359, 367), "TIS620"); 314 | personalInformation.ExpireDate = new String(Arrays.copyOfRange(buffer, 367, 375), "TIS620"); 315 | personalInformation.Address = new String(addressBuffer, "TIS620"); 316 | personalInformation.PictureTag = new String(Arrays.copyOfRange(data.data, 160, 174), "TIS620"); 317 | } catch (UnsupportedEncodingException e) { 318 | Log.w(TAG, "Cannot decode TIS620 string"); 319 | return null; 320 | } 321 | 322 | return personalInformation; 323 | } 324 | 325 | public class ChipCardADM { 326 | public String Version; 327 | public String State; 328 | public String Authorize; 329 | public String LaserNumber; 330 | } 331 | 332 | public ChipCardADM getChipCardADM() { 333 | SmartCardMessage.DataBlock data; 334 | ChipCardADM chipCardADM; 335 | 336 | byte[] message = new byte[]{(byte)0x80, (byte)0x00, (byte)0x00, (byte)0x00}; 337 | 338 | if (!this.selectAppletExtension()) { 339 | Log.d(TAG, "selectAppletExtension fail"); 340 | return null; 341 | } 342 | 343 | if ((data = this.getCardData(message)) == null) { 344 | Log.d(TAG, "Get card information block 1-1 failed"); 345 | return null; 346 | } 347 | 348 | if (data.status != 0 || data.error != 0 || data.data.length != 0x17 + 2) { 349 | Log.w(TAG, String.format("Invalid card ADM response [%d][%d][%d][%s]", data.status, data.error, data.data.length, this.byteArrayToHexString(data.data))); 350 | return null; 351 | } 352 | 353 | chipCardADM = new ChipCardADM(); 354 | 355 | chipCardADM.Version = String.format(Locale.US, "%d.%d", data.data[0], data.data[1]); 356 | chipCardADM.State = String.format(Locale.US, "%d", data.data[2]); 357 | chipCardADM.Authorize = String.format(Locale.US, "%d", data.data[3]); 358 | chipCardADM.LaserNumber = new String(Arrays.copyOfRange(data.data, 7,23)); 359 | 360 | return chipCardADM; 361 | } 362 | 363 | public Bitmap getPersonalPicture() { 364 | SmartCardMessage.DataBlock data; 365 | byte[] message = new byte[]{(byte)0x80, (byte)0xb0, (byte)0x00, (byte)0x00, (byte)0x02, (byte)0x00, (byte)0x00}; 366 | byte[] pictureBuffer; 367 | 368 | if (!this.selectAppletStorageData()) { 369 | Log.d(TAG, "selectAppletStorageData fail"); 370 | return null; 371 | } 372 | 373 | int offset = 0x017b, blockNumber = 1, blockLength, index = 0; 374 | ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 375 | 376 | int PERSONAL_PIC_LENGTH = 5118; 377 | 378 | while (index < PERSONAL_PIC_LENGTH) { 379 | blockLength = ((PERSONAL_PIC_LENGTH - index) > 0xff) ? 0xff:(PERSONAL_PIC_LENGTH - index); 380 | 381 | message[2] = (byte)((offset >> 8) & 0xff); 382 | message[3] = (byte)(offset & 0xff); 383 | message[6] = (byte)blockLength; 384 | 385 | if ((data = this.getCardData(message)) == null) { 386 | Log.w(TAG, "Get personal picture block [" + blockNumber + "] failed"); 387 | return null; 388 | } 389 | 390 | if (data.status != 0 || data.error != 0) { 391 | Log.w(TAG, String.format("Invalid personal picture response [%d][%d][%d][%d][%s]", blockNumber, data.status, data.error, data.data.length, this.byteArrayToHexString(data.data))); 392 | return null; 393 | } 394 | 395 | if (data.data.length != blockLength + 2) { 396 | Log.w(TAG, "Get personal picture block [" + blockNumber + "] return invalid length [" + data.data.length + "/" + blockLength + "]"); 397 | return null; 398 | } 399 | 400 | buffer.write(data.data, 0, blockLength); 401 | 402 | offset += blockLength; 403 | index += blockLength; 404 | blockNumber++; 405 | } 406 | 407 | pictureBuffer = buffer.toByteArray(); 408 | 409 | while (index >= 0 && pictureBuffer[index - 1] == 0x20) index--; 410 | 411 | return BitmapFactory.decodeByteArray(pictureBuffer, 0, index); 412 | } 413 | 414 | private abstract class SimpleRequest { 415 | String url; 416 | String response = null; 417 | 418 | SimpleRequest(String url) { 419 | this.url = url; 420 | } 421 | 422 | abstract void onResponse(String response); 423 | abstract void onError(); 424 | } 425 | 426 | public interface VerifyPinCallback { 427 | void onSuccess(); 428 | void onFailed(int remain); 429 | void onError(); 430 | } 431 | 432 | // true = done, false = error or exception 433 | public boolean verifyPinCode(String pin, final VerifyPinCallback verifyPinCallback) { 434 | SmartCardMessage.DataBlock data; 435 | byte[] message; 436 | byte[] expandPin = new byte[] { 437 | (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, 438 | (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, 439 | (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, 440 | (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff 441 | }; 442 | byte[] sha1Hash; 443 | byte[] deriveKey = new byte[]{ 444 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 445 | 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 446 | 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 447 | 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F 448 | }; 449 | byte[] answerData = new byte[32]; 450 | byte[] inputData = new byte[32]; 451 | byte[] iv = new byte[8]; 452 | byte[] keyBytes = new byte[24]; 453 | SecretKey secretKey; 454 | IvParameterSpec ivParam; 455 | Cipher cipher; 456 | 457 | if (pin == null || pin.length() != 4) { 458 | Log.w(TAG, "Invalid pin code"); 459 | return false; 460 | } 461 | 462 | if (verifyPinCallback == null) { 463 | Log.w(TAG, "Invalid callback"); 464 | return false; 465 | } 466 | 467 | if (!this.selectAppletExtension()) { 468 | Log.d(TAG, "selectAppletExtension fail"); 469 | return false; 470 | } 471 | 472 | // pin challenge 473 | message = new byte[]{(byte)0x80, (byte)0xb4, (byte)0x00, (byte)0x00}; 474 | 475 | if ((data = this.getCardData(message)) == null) { 476 | Log.w(TAG, "Get pin challenge failed"); 477 | return false; 478 | } 479 | 480 | if (data.status != 0 || data.error != 0) { 481 | Log.w(TAG, String.format("Invalid pin challenge response [%d][%d][%d][%s]", data.status, data.error, data.data.length, this.byteArrayToHexString(data.data))); 482 | return false; 483 | } 484 | 485 | if (data.data.length != 0x20 + 2) { 486 | Log.w(TAG, String.format("Invalid pin challenge length [%d]", data.data.length)); 487 | return false; 488 | } 489 | 490 | for (int i = 0; i < 4; i++) { 491 | expandPin[i] = (byte)pin.charAt(i); 492 | } 493 | 494 | try { 495 | MessageDigest digest = MessageDigest.getInstance("SHA-1"); 496 | 497 | sha1Hash = digest.digest(expandPin); 498 | } catch (NoSuchAlgorithmException e) { 499 | Log.w(TAG, "SHA-1 algorithm not support"); 500 | return false; 501 | } 502 | 503 | for (int i = 0, j = 0; i < 20; i += 4, j += 7) { 504 | System.arraycopy(sha1Hash, i, deriveKey, j, 4); 505 | } 506 | 507 | System.arraycopy(data.data,0, inputData, 0, 0x20); 508 | 509 | for (int i = 0; i <= 16; i += 8) { 510 | Arrays.fill(iv, (byte)0); 511 | System.arraycopy(deriveKey, i, keyBytes, 0, 8); 512 | System.arraycopy(deriveKey, i + 8, keyBytes, 8, 8); 513 | System.arraycopy(deriveKey, i, keyBytes, 16, 8); 514 | 515 | secretKey = new SecretKeySpec(keyBytes, "DESede"); 516 | ivParam = new IvParameterSpec(iv); 517 | 518 | try { 519 | cipher = Cipher.getInstance("DESede/CBC/NoPadding"); 520 | } catch (NoSuchPaddingException e) { 521 | Log.w(TAG, "NoSuchPaddingException"); 522 | return false; 523 | } catch (NoSuchAlgorithmException e) { 524 | Log.w(TAG, "NoSuchAlgorithmException"); 525 | return false; 526 | } 527 | 528 | try { 529 | cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParam); 530 | } catch (InvalidAlgorithmParameterException e) { 531 | Log.w(TAG, "InvalidAlgorithmParameterException"); 532 | return false; 533 | } catch (InvalidKeyException e) { 534 | Log.w(TAG, "InvalidKeyException"); 535 | return false; 536 | } 537 | 538 | try { 539 | answerData = cipher.doFinal(inputData); 540 | } catch (IllegalBlockSizeException e) { 541 | Log.w(TAG, "IllegalBlockSizeException"); 542 | return false; 543 | } catch (BadPaddingException e) { 544 | Log.w(TAG, "BadPaddingException"); 545 | return false; 546 | } 547 | 548 | if (answerData.length == 32){ 549 | System.arraycopy(answerData, 0, inputData, 0, 0x20); 550 | } else if (answerData.length == 40) { 551 | System.arraycopy(answerData, 8, inputData, 0, 0x20); 552 | } else { 553 | Log.w(TAG, "Invalid cipher caculate"); 554 | return false; 555 | } 556 | } 557 | 558 | message = new byte[]{(byte)0x80, (byte)0x20, (byte)0x01, (byte)0x00, (byte)0x20, 559 | (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, 560 | (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, 561 | (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, 562 | (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00}; 563 | 564 | System.arraycopy(answerData, 0, message, 5, answerData.length); 565 | 566 | if ((data = ThaiSmartCard.this.device.sendAPDU(message)) == null) { 567 | Log.w(TAG, "Send pin verify failed"); 568 | return false; 569 | } 570 | 571 | if (data.status != 0 || data.error != 0) { 572 | Log.w(TAG, String.format("Invalid verify pin response [%d][%d][%d][%s]", data.status, data.error, data.data.length, ThaiSmartCard.this.byteArrayToHexString(data.data))); 573 | return false; 574 | } 575 | 576 | if (data.data[0] == (byte)0x63) { 577 | Log.w(TAG, String.format("PIN verify incorrect, remaining [%d]", data.data[1])); 578 | verifyPinCallback.onFailed(data.data[1]); 579 | } else if (data.data[0] == (byte)0x90) { 580 | verifyPinCallback.onSuccess(); 581 | } else { 582 | Log.w(TAG, String.format("Invalid verify pin response data [%d][%s]", data.data.length, ThaiSmartCard.this.byteArrayToHexString(data.data))); 583 | verifyPinCallback.onError(); 584 | } 585 | 586 | return true; 587 | } 588 | 589 | public enum VerifyResult { 590 | NOT_ALLOW(-2), 591 | ERROR(-1), 592 | SUCCESS(0), 593 | TIMEOUT(1); 594 | 595 | private final int value; 596 | VerifyResult(int value) { this.value = value; } 597 | public int getValue() { return this.value; } 598 | } 599 | 600 | public VerifyResult verifyFingerPrint(int maxRetry) { 601 | int i = 0; 602 | SmartCardMessage.DataBlock data; 603 | SmartCardMessage.EscapeResponseBlock escape; 604 | 605 | // test on precise 200-250 MC only 606 | if (!this.device.getDeviceProductName().contains("Precise")) { 607 | return VerifyResult.NOT_ALLOW; 608 | } 609 | 610 | if (!this.selectAppletBio()) { 611 | Log.d(TAG, "selectAppletBio fail"); 612 | return VerifyResult.ERROR; 613 | } 614 | 615 | // get fingerprint parameter 616 | byte[] message = new byte[]{(byte)0xb0, (byte)0x34, (byte)0x00, (byte)0x76}; 617 | 618 | if ((data = this.device.sendAPDU(message)) == null) { 619 | Log.w(TAG, "APDU get fingerprint parameter failed"); 620 | return VerifyResult.ERROR; 621 | } 622 | 623 | if (data.error != 0 || data.status != 0) { 624 | Log.w(TAG, "APDU get fingerprint parameter return abnormal status [" + data.status + ":" + data.error + "]"); 625 | return VerifyResult.ERROR; 626 | } 627 | 628 | if (data.data == null || data.data.length != 0x78) { 629 | Log.w(TAG, "APDU get fingerprint parameter invalid data: " + ((data.data != null) ? data.data.length: 0)); 630 | return VerifyResult.ERROR; 631 | } 632 | 633 | byte[] param = new byte[0x76]; 634 | 635 | System.arraycopy(data.data, 0, param, 0, 0x76); 636 | 637 | 638 | 639 | // set fingerprint parameter 640 | if ((escape = this.device.sendEscapeCommand(message)) == null) { 641 | Log.w(TAG, "Escape command set fingerprint parameter 1 failed"); 642 | return VerifyResult.ERROR; 643 | } 644 | 645 | if (escape.error != 0 || escape.status != 0) { 646 | Log.w(TAG, "Escape command set fingerprint parameter 1 return abnormal status [" + escape.status + ":" + escape.error + "]"); 647 | return VerifyResult.ERROR; 648 | } 649 | 650 | if (escape.data.length != 3 || escape.data[0] != 0x00 || escape.data[1] != 0x00 || escape.data[2] != 0x00) { 651 | Log.w(TAG, "Escape command set fingerprint parameter 1 invalid data: " + escape.data.length + " " + String.format("%02d %02d %02d", escape.data[0], escape.data[1], escape.data[2])); 652 | return VerifyResult.ERROR; 653 | } 654 | 655 | // set parameter ?? 656 | message = new byte[]{(byte)0x00, (byte)0xc8, (byte)0x00}; 657 | 658 | if ((escape = this.device.sendEscapeCommand(message)) == null) { 659 | Log.w(TAG, "Escape command set fingerprint parameter 2 failed"); 660 | return VerifyResult.ERROR; 661 | } 662 | 663 | if (escape.error != 0 || escape.status != 0) { 664 | Log.w(TAG, "Escape command set fingerprint parameter 2 return abnormal status [" + escape.status + ":" + escape.error + "]"); 665 | return VerifyResult.ERROR; 666 | } 667 | 668 | while (i < maxRetry) { 669 | // 670 | 671 | i++; 672 | } 673 | 674 | return VerifyResult.ERROR; /// 675 | } 676 | 677 | } 678 | 679 | -------------------------------------------------------------------------------- /thainationalidcard/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidThaiNationalIDCard 3 | 4 | -------------------------------------------------------------------------------- /thainationalidcard/src/test/java/co/advancedlogic/thainationalidcard/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package co.advancedlogic.thainationalidcard; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } --------------------------------------------------------------------------------