├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 | }
--------------------------------------------------------------------------------