From 43f9ff6b8281a225a8aff8cbc8b8512938123cd6 Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 30 Jul 2025 10:14:00 +0900 Subject: [PATCH] First --- .gitignore | 50 +++ .idea/.gitignore | 3 + .idea/AndroidProjectSystem.xml | 6 + .idea/compiler.xml | 6 + .idea/deploymentTargetSelector.xml | 18 + .idea/gradle.xml | 19 ++ .idea/migrations.xml | 10 + .idea/misc.xml | 9 + .idea/runConfigurations.xml | 17 + .idea/vcs.xml | 6 + app/.gitignore | 1 + app/build.gradle.kts | 44 +++ app/proguard-rules.pro | 21 ++ .../idcard/ExampleInstrumentedTest.java | 26 ++ app/src/main/AndroidManifest.xml | 37 +++ .../java/com/example/idcard/ApduService.java | 238 ++++++++++++++ .../com/example/idcard/EmulatorFragment.java | 175 ++++++++++ .../java/com/example/idcard/MainActivity.java | 58 ++++ .../com/example/idcard/ReaderFragment.java | 311 ++++++++++++++++++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++ .../res/drawable/ic_launcher_foreground.xml | 30 ++ app/src/main/res/layout/activity_main.xml | 48 +++ app/src/main/res/layout/dialog_settings.xml | 120 +++++++ app/src/main/res/layout/fragment_emulator.xml | 53 +++ app/src/main/res/layout/fragment_reader.xml | 29 ++ .../main/res/mipmap-anydpi/ic_launcher.xml | 6 + .../res/mipmap-anydpi/ic_launcher_round.xml | 6 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes app/src/main/res/values-night/themes.xml | 16 + app/src/main/res/values/colors.xml | 10 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/themes.xml | 16 + app/src/main/res/xml/apduservice.xml | 7 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 ++ app/src/main/res/xml/nfc_tech_filter.xml | 5 + .../com/example/idcard/ExampleUnitTest.java | 17 + build.gradle.kts | 4 + gradle.properties | 21 ++ gradle/libs.versions.toml | 18 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 +++++++++++ gradlew.bat | 89 +++++ settings.gradle.kts | 24 ++ 54 files changed, 1970 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/migrations.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/example/idcard/ExampleInstrumentedTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/example/idcard/ApduService.java create mode 100644 app/src/main/java/com/example/idcard/EmulatorFragment.java create mode 100644 app/src/main/java/com/example/idcard/MainActivity.java create mode 100644 app/src/main/java/com/example/idcard/ReaderFragment.java create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/dialog_settings.xml create mode 100644 app/src/main/res/layout/fragment_emulator.xml create mode 100644 app/src/main/res/layout/fragment_reader.xml create mode 100644 app/src/main/res/mipmap-anydpi/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/values-night/themes.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/apduservice.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/main/res/xml/nfc_tech_filter.xml create mode 100644 app/src/test/java/com/example/idcard/ExampleUnitTest.java create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e70f586 --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.aab +*.apk +output-metadata.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b127739 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..f9e17b1 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace = "com.example.idcard" + compileSdk = 36 + + defaultConfig { + applicationId = "com.example.idcard" + minSdk = 32 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +dependencies { + implementation(libs.appcompat) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.ext.junit) + androidTestImplementation(libs.espresso.core) + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.9.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.fragment:fragment:1.6.1") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/idcard/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/example/idcard/ExampleInstrumentedTest.java new file mode 100644 index 0000000..9182cf3 --- /dev/null +++ b/app/src/androidTest/java/com/example/idcard/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.idcard; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.idcard", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ac37660 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/idcard/ApduService.java b/app/src/main/java/com/example/idcard/ApduService.java new file mode 100644 index 0000000..47502de --- /dev/null +++ b/app/src/main/java/com/example/idcard/ApduService.java @@ -0,0 +1,238 @@ +package com.example.idcard; + +import android.content.SharedPreferences; +import android.nfc.cardemulation.HostApduService; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.widget.Toast; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.UUID; + +public class ApduService extends HostApduService { + private static final String TAG = "SNU_APDU"; + public static boolean isAppActive = false; + public static EmulatorFragment emulatorFragment = null; + + private static final byte[] ERROR_6400 = {(byte)0x64, 0x00}; + private static final byte[] ERROR_6300 = {(byte)0x63, 0x00}; + private static final byte[] SUCCESS_9000 = {(byte)0x90, 0x00}; + private String currentChallenge = ""; + private SharedPreferences prefs; + + private enum COMMAND { + SELECT, + GET_CHALLENGE, + CREATE_SESSION, + EXTERNAL_AUTH, + READ_RECORD, + UNKNOWN + } + + @Override + public byte[] processCommandApdu(byte[] apdu, Bundle extras) { + if (!isAppActive) { + addLog("App not active, ignoring APDU"); + Log.d(TAG, "App not active, ignoring APDU"); + return ERROR_6400; + } + + prefs = getSharedPreferences("user_info", MODE_PRIVATE); + + String receivedHex = bytesToHex(apdu); + Log.d(TAG, "RX: " + receivedHex); + addLog("RX: " + receivedHex); + + if (apdu == null || apdu.length < 2) { + return ERROR_6400; + } + + byte cla = apdu[0]; + byte ins = apdu[1]; + + COMMAND command = COMMAND.UNKNOWN; + if (cla == 0x00 && ins == (byte)0xA4) { + command = COMMAND.SELECT; + } else if (cla == 0x00 && ins == (byte)0x84) { + command = COMMAND.GET_CHALLENGE; + } else if (cla == (byte)0x90 && ins == (byte)0x8A) { + command = COMMAND.CREATE_SESSION; + } else if ((cla == 0x00 || cla == (byte)0x94) && ins == (byte)0x82) { + command = COMMAND.EXTERNAL_AUTH; + } else if (cla == 0x00 && ins == (byte)0xB2) { + command = COMMAND.READ_RECORD; + } + + byte[] result; + switch (command) { + case SELECT: + String tagBF0C = buildTlv("BF0C", "01000000000000"); + String tag50 = buildTlv("50", stringToHex("USIM ID ")); + String tag84 = buildTlv("84", "D410000005494401"); + String tagA5 = buildTlv("A5", tag50 + tagBF0C); + String tag6F = buildTlv("6F", tag84 + tagA5); + result = hexToBytes(tag6F + "9000"); + break; + + case GET_CHALLENGE: + String challenge = prefs.getString("CHALLENGE", ""); + if (challenge == "") { + challenge = UUID.randomUUID().toString().replace("-", ""); + } else if (challenge == "0") { + challenge = "00000000000000000000000000000000"; + } else if (challenge == "X") { + challenge = ""; + } else if (challenge.length() != 32) { + challenge = padHexRight(challenge, 16, "00"); + } + result = hexToBytes(challenge + "9000"); + currentChallenge = challenge; + break; + + case CREATE_SESSION: + result = SUCCESS_9000; + String hostChallenge = bytesToHex(Arrays.copyOfRange(apdu, 5, apdu.length - 4)); + String MAC = bytesToHex(Arrays.copyOfRange(apdu, apdu.length - 4, apdu.length)); + String log = String.format("SESSION: HOST %s CARD %s -> %s", hostChallenge, currentChallenge, MAC); + Log.d(TAG, log); + addLog(log); + break; + + case EXTERNAL_AUTH: + result = SUCCESS_9000; + break; + + case READ_RECORD: + String univCd = prefs.getString("UNIVCD", "0345"); + String cardTypeCd = prefs.getString("CARDTYPECD", "1"); + String userId = prefs.getString("USERID", ""); + String cardIssueNo = prefs.getString("CARDISSUENO", "0"); + String userName = prefs.getString("USERNAME", ""); + String userMainId = prefs.getString("USERMAINID", ""); + + if (cardTypeCd.length() > 1) { + cardTypeCd = cardTypeCd.substring(1); + } + + int issueNoInt = 0; + try { + issueNoInt = Integer.parseInt(cardIssueNo); + } catch (Exception e) { + issueNoInt = 0; + } + + String f01 = padHexRight(stringToHex(univCd), 10, "20"); + String f03 = stringToHex(cardTypeCd); + String f04 = padHexRight(stringToHex(userId), 30, "20"); + String f05 = String.format("%02X", issueNoInt); + String f06 = padHexRight(stringToHexEucKr(userName), 40, "20"); + String f07 = padHexRight(stringToHex(userMainId), 26, "20"); + String f08 = "00".repeat(24); + String f09 = "00".repeat(103); + + String recordResult = "01C60205" + f01 + + "0301" + f03 + + "040F" + f04 + + "0501" + f05 + + "0614" + f06 + + "070D" + f07 + + "0818" + f08 + + "0967" + f09 + + "9000"; + + result = hexToBytes(recordResult); + showCardReadAlert(); + break; + + case UNKNOWN: + default: + result = ERROR_6400; + break; + } + + String responseHex = bytesToHex(result); + Log.d(TAG, "TX: " + responseHex); + addLog("TX: " + responseHex); + return result; + } + + private void addLog(String message) { + if (emulatorFragment != null) { + emulatorFragment.addLogMessage(message); + } + } + + private void showCardReadAlert() { + Handler mainHandler = new Handler(Looper.getMainLooper()); + mainHandler.post(() -> { + Toast.makeText(this, "Card data was read!", Toast.LENGTH_LONG).show(); + }); + } + + private String buildTlv(String tag, String value) { + int length = value.length() / 2; + if (length < 128) { + return tag + String.format("%02X", length) + value; + } else if (length < 256) { + return tag + "81" + String.format("%02X", length) + value; + } else if (length < 65536) { + return tag + String.format("82%02X%02X", length >> 8, length & 0xFF) + value; + } + return ""; + } + + private String stringToHex(String str) { + StringBuilder hex = new StringBuilder(); + for (char c : str.toCharArray()) { + hex.append(String.format("%02X", (int) c)); + } + return hex.toString(); + } + + private String stringToHexEucKr(String str) { + try { + byte[] bytes = str.getBytes("EUC-KR"); + StringBuilder hex = new StringBuilder(); + for (byte b : bytes) { + hex.append(String.format("%02X", b & 0xFF)); + } + return hex.toString(); + } catch (UnsupportedEncodingException e) { + return stringToHex(str); + } + } + + private String padHexRight(String hexStr, int totalLen, String pad) { + int needed = totalLen - hexStr.length(); + if (needed <= 0) { + return hexStr.substring(0, totalLen); + } + return hexStr + pad.repeat(needed / 2); + } + + private byte[] hexToBytes(String hex) { + int len = hex.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + + Character.digit(hex.charAt(i+1), 16)); + } + return data; + } + + private String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02X", b & 0xFF)); + } + return result.toString(); + } + + @Override + public void onDeactivated(int reason) { + Log.d(TAG, "Deactivated: " + reason); + addLog("NFC Deactivated: " + reason); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/idcard/EmulatorFragment.java b/app/src/main/java/com/example/idcard/EmulatorFragment.java new file mode 100644 index 0000000..b79dda2 --- /dev/null +++ b/app/src/main/java/com/example/idcard/EmulatorFragment.java @@ -0,0 +1,175 @@ +package com.example.idcard; + +import android.Manifest; +import android.app.Activity; +import android.app.Dialog; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; +import androidx.fragment.app.Fragment; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import static android.content.Context.MODE_PRIVATE; + +public class EmulatorFragment extends Fragment { + private TextView tvLog; + private ScrollView scrollView; + private final StringBuilder logBuffer = new StringBuilder(); + private SharedPreferences prefs; + private static final int STORAGE_PERMISSION_CODE = 1001; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + prefs = requireActivity().getSharedPreferences("user_info", MODE_PRIVATE); + + View view = inflater.inflate(R.layout.fragment_emulator, container, false); + + tvLog = view.findViewById(R.id.tv_log); + scrollView = view.findViewById(R.id.scroll_view); + Button btnClearLog = view.findViewById(R.id.btn_clear_log); + Button btnSettings = view.findViewById(R.id.btn_settings); + btnSettings.setOnClickListener(v -> showSettingsDialog()); + btnClearLog.setOnClickListener(v -> clearLog()); + checkStoragePermission(); + addLogMessage("Emulator Started"); + return view; + } + + @Override + public void onResume() { + super.onResume(); + ApduService.isAppActive = true; + ApduService.emulatorFragment = this; + } + + @Override + public void onPause() { + super.onPause(); + ApduService.isAppActive = false; + } + + @Override + public void onDestroy() { + super.onDestroy(); + ApduService.isAppActive = false; + ApduService.emulatorFragment = null; + } + + public void addLogMessage(String message) { + String timestamp = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(new Date()); + String logEntry = timestamp + " - " + message + "\n"; + writeLogToFile(logEntry); + Activity activity = getActivity(); + if (activity != null) { + activity.runOnUiThread(() -> { + logBuffer.append(logEntry); + tvLog.setText(logBuffer.toString()); + scrollView.post(() -> scrollView.fullScroll(ScrollView.FOCUS_DOWN)); + }); + } + } + + private void writeLogToFile(String text) { + File downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + File logFile = new File(downloadDir, "NFC_LOG.txt"); + + try (FileWriter writer = new FileWriter(logFile, true); // true = append + BufferedWriter bufferedWriter = new BufferedWriter(writer)) { + bufferedWriter.write(text); + } catch (IOException e) { + Log.e("FileLogger", "Error writing to log file", e); + } + } + + private void checkStoragePermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (requireActivity().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + STORAGE_PERMISSION_CODE); + } + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == STORAGE_PERMISSION_CODE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + } else { + Toast.makeText(getActivity(), "permission denied", Toast.LENGTH_SHORT).show(); + } + } + } + + private void clearLog() { + logBuffer.setLength(0); + tvLog.setText(""); + } + + private void showSettingsDialog() { + Dialog dialog = new Dialog(getActivity()); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + dialog.setContentView(R.layout.dialog_settings); + dialog.setCancelable(true); + + EditText etUnivCd = dialog.findViewById(R.id.et_univ_cd); + EditText etCardTypeCd = dialog.findViewById(R.id.et_card_type_cd); + EditText etUserId = dialog.findViewById(R.id.et_user_id); + EditText etCardIssueNo = dialog.findViewById(R.id.et_card_issue_no); + EditText etUserName = dialog.findViewById(R.id.et_user_name); + EditText etUserMainId = dialog.findViewById(R.id.et_user_main_id); + EditText etChallenge = dialog.findViewById(R.id.et_challenge); + Button btnSave = dialog.findViewById(R.id.btn_save); + Button btnCancel = dialog.findViewById(R.id.btn_cancel); + + etUnivCd.setText(prefs.getString("UNIVCD", "0345")); + etCardTypeCd.setText(prefs.getString("CARDTYPECD", "1")); + etUserId.setText(prefs.getString("USERID", "")); + etCardIssueNo.setText(prefs.getString("CARDISSUENO", "1")); + etUserName.setText(prefs.getString("USERNAME", "")); + etUserMainId.setText(prefs.getString("USERMAINID", "")); + etChallenge.setText(prefs.getString("CHALLENGE", "")); + + btnSave.setOnClickListener(v -> { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString("UNIVCD", etUnivCd.getText().toString()); + editor.putString("CARDTYPECD", etCardTypeCd.getText().toString()); + editor.putString("USERID", etUserId.getText().toString()); + editor.putString("CARDISSUENO", etCardIssueNo.getText().toString()); + editor.putString("USERNAME", etUserName.getText().toString()); + editor.putString("USERMAINID", etUserMainId.getText().toString()); + editor.putString("CHALLENGE", etChallenge.getText().toString()); + editor.apply(); + Toast.makeText(getActivity(), "Settings saved", Toast.LENGTH_SHORT).show(); + dialog.dismiss(); + }); + btnCancel.setOnClickListener(v -> dialog.dismiss()); + + dialog.show(); + if (dialog.getWindow() != null) { + dialog.getWindow().setLayout( + (int) (getResources().getDisplayMetrics().widthPixels * 0.9), + android.view.ViewGroup.LayoutParams.WRAP_CONTENT + ); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/idcard/MainActivity.java b/app/src/main/java/com/example/idcard/MainActivity.java new file mode 100644 index 0000000..5fde8fd --- /dev/null +++ b/app/src/main/java/com/example/idcard/MainActivity.java @@ -0,0 +1,58 @@ +package com.example.idcard; + +import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import com.google.android.material.tabs.TabLayout; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + TabLayout tabLayout = findViewById(R.id.menu_tab); + + if (savedInstanceState == null) { + loadFragment(new EmulatorFragment()); + } + + tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + Fragment selectedFragment = null; + switch (tab.getPosition()) { + case 0: + selectedFragment = new EmulatorFragment(); + break; + case 1: + selectedFragment = new ReaderFragment(); + break; + } + loadFragment(selectedFragment); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + } + }); + } + + private boolean loadFragment(Fragment fragment) { + if (fragment != null) { + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction transaction = fragmentManager.beginTransaction(); + transaction.replace(R.id.fragment_container, fragment); + transaction.commit(); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/idcard/ReaderFragment.java b/app/src/main/java/com/example/idcard/ReaderFragment.java new file mode 100644 index 0000000..63beccd --- /dev/null +++ b/app/src/main/java/com/example/idcard/ReaderFragment.java @@ -0,0 +1,311 @@ +package com.example.idcard; + +import android.nfc.NfcAdapter; +import android.nfc.Tag; +import android.nfc.tech.IsoDep; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; +import androidx.fragment.app.Fragment; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; + +public class ReaderFragment extends Fragment implements NfcAdapter.ReaderCallback { + private static final String TAG = "NFCReader"; + private NfcAdapter nfcAdapter; + private TextView resultText; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_reader, container, false); + + resultText = view.findViewById(R.id.resultText); + nfcAdapter = NfcAdapter.getDefaultAdapter(getActivity()); + + if (nfcAdapter == null) { + Toast.makeText(getActivity(), "NFC not supported", Toast.LENGTH_SHORT).show(); + return view; + } + + if (!nfcAdapter.isEnabled()) { + Toast.makeText(getActivity(), "Please enable NFC in settings", Toast.LENGTH_LONG).show(); + } + + updateResult("Reader ready..."); + return view; + } + + @Override + public void onResume() { + super.onResume(); + if (nfcAdapter != null && nfcAdapter.isEnabled()) { + Bundle options = new Bundle(); + options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250); + + nfcAdapter.enableReaderMode(getActivity(), this, + NfcAdapter.FLAG_READER_NFC_A | + NfcAdapter.FLAG_READER_NFC_B | + NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, + options); + + updateResult("Scanning..."); + } + } + + @Override + public void onPause() { + super.onPause(); + if (nfcAdapter != null) { + nfcAdapter.disableReaderMode(getActivity()); + } + } + + @Override + public void onTagDiscovered(Tag tag) { + if (getActivity() != null) { + getActivity().runOnUiThread(() -> updateResult("Card detected! Reading...")); + readNFCCard(tag); + } + } + + public byte[] ApduRequest(IsoDep isoDep, StringBuilder result, String command) throws IOException { + byte[] cmd = hexToBytes(command); + byte[] resp = isoDep.transceive(cmd); + result.append("Cmd: ").append(bytesToHex(cmd)).append("\n"); + result.append("Resp: ").append(bytesToHex(resp)).append("\n"); + result.append("Status: ").append(getStatusWord(resp)).append("\n"); + result.append("\n"); + return resp; + } + + private void readNFCCard(Tag tag) { + IsoDep isoDep = IsoDep.get(tag); + if (isoDep == null) { + getActivity().runOnUiThread(() -> updateResult("Card not compatible")); + return; + } + + try { + isoDep.connect(); + isoDep.setTimeout(5000); // Increased timeout + + StringBuilder result = new StringBuilder("=== RESULT ===\n\n"); + + // 1. SELECT Application + result.append("1. SELECT Student App:\n\n"); + byte[] selectResp = + ApduRequest(isoDep, result, "00 A4 04 00 08 D4 10 00 00 05 49 44 01"); + + if (isSuccess(selectResp)) { + parseSelectResponse(selectResp, result); + result.append("-- Application found\n\n"); + + // 2. GET CHALLENGE + result.append("2. GET CHALLENGE:\n\n"); + byte[] challengeResp = + ApduRequest(isoDep, result, "00 84 00 00 10"); + if (isSuccess(challengeResp)) { + result.append("-- Challenge: ").append(extractChallenge(challengeResp)).append("\n\n"); + } + result.append("\n"); + // GET CHALLENGE END + + // cardChallenge + + // 3. CREATE SESSION + result.append("3. CREATE SESSION:\n\n"); + byte[] sessionResp = + ApduRequest(isoDep, result, "80 8A 00 81 14" + "01 02 03 04 01 02 03 04 01 02 03 04" + "01 02 03 04"); + if (isSuccess(sessionResp)) { + result.append("-- Session created\n"); + } else { + result.append("-- Session Error\n"); + return; + } + result.append("\n"); + // CREATE SESSION END + + // 4-1. GET CHALLENGE + result.append("2. GET CHALLENGE:\n\n"); + ApduRequest(isoDep, result, "0084000010"); + // GET CHALLENGE END + +// // 4. EXTERNAL AUTH +// result.append("4. EXTERNAL AUTH:\n\n"); +// byte[] authResp = +// ApduRequest(isoDep, result, "00 82 00 82 04" + "01 01 01 01"); +// if (isSuccess(authResp)) { +// result.append("-- Authentication successful\n"); +// } else { +// result.append("-- Authentication failed\n"); +// return +// } +// result.append("\n"); +// // EXTERNAL AUTH END +// +// // 5. READ RECORD END +// result.append("5. READ RECORD:\n\n"); +// boolean recordSuccess = false; +// String[] readCommands = { +// "00B2010CC8", +// }; +// for (String cmdHex : readCommands) { +// byte[] readResp = +// ApduRequest(isoDep, result, cmdHex); +// if (isSuccess(readResp)) { +// result.append("-- Data: ").append(bytesToHex(readResp)).append("\n"); +// recordSuccess = true; +// break; +// } +// else if (readResp.length >= 2 && +// readResp[readResp.length-2] == (byte)0x61) { +// +// // 61XX = More data available +// byte sw2 = readResp[readResp.length-1]; +// result.append(" (More data available: ").append(String.format("%02X", sw2 & 0xFF)).append(")\n"); +// +// // Send GET RESPONSE +// byte[] getResponseCmd = hexToBytes("00C00000" + String.format("%02X", sw2 & 0xFF)); +// byte[] moreData = isoDep.transceive(getResponseCmd); +// result.append("GET RESPONSE: ").append(bytesToHex(moreData)).append("\n"); +// +// if (isSuccess(moreData)) { +// recordSuccess = true; +// break; +// } +// } +// else { +// result.append("\n"); +// } +// } +// if (!recordSuccess) { +// result.append("-- File not found\n"); +// } +// result.append("\n"); +// // READ RECORD END + + } else { + result.append("-- Application not found\n\n"); + } + + isoDep.close(); + + final String finalResult = result.toString(); + getActivity().runOnUiThread(() -> updateResult(finalResult)); + + } catch (IOException e) { + Log.e(TAG, "Error reading card", e); + getActivity().runOnUiThread(() -> updateResult("Error: " + e.getMessage())); + } + } + + private void parseSelectResponse(byte[] response, StringBuilder result) { + String hex = bytesToHex(response); + if (hex.length() >= 4) { + hex = hex.substring(0, hex.length() - 4); + } + if (hex.contains("6F")) { + result.append("-- 6F (File Control Information)\n"); + } + if (hex.contains("84")) { + result.append("-- 84 (Application Identifier)\n"); + } + if (hex.contains("50")) { + result.append("-- 50 (Application Label)\n"); + int pos = hex.indexOf("50"); + if (pos >= 0 && pos + 4 < hex.length()) { + try { + int len = Integer.parseInt(hex.substring(pos + 2, pos + 4), 16); + if (pos + 4 + len * 2 <= hex.length()) { + String labelHex = hex.substring(pos + 4, pos + 4 + len * 2); + String label = hexToString(labelHex); + result.append(" -- Label: '").append(label.trim()).append("'\n"); + } + } catch (Exception e) { + Log.e(TAG, "Error parsing label", e); + } + } + } + result.append("\n"); + } + + private String extractChallenge(byte[] response) { + if (response.length >= 4) { + byte[] challenge = new byte[response.length - 2]; + System.arraycopy(response, 0, challenge, 0, response.length - 2); + return bytesToHex(challenge); + } + return "No data"; + } + + private boolean isSuccess(byte[] response) { + return response.length >= 2 && + response[response.length - 2] == (byte) 0x90 && + response[response.length - 1] == (byte) 0x00; + } + + private String getStatusWord(byte[] response) { + if (response.length >= 2) { + byte sw1 = response[response.length - 2]; + byte sw2 = response[response.length - 1]; + String status = String.format("%02X%02X", sw1 & 0xFF, sw2 & 0xFF); + + if (sw1 == (byte) 0x90 && sw2 == (byte) 0x00) return status + " (Success)"; + if (sw1 == (byte) 0x6A && sw2 == (byte) 0x82) return status + " (File not found)"; + if (sw1 == (byte) 0x6A && sw2 == (byte) 0x86) return status + " (Wrong parameters)"; + if (sw1 == (byte) 0x69 && sw2 == (byte) 0x82) return status + " (Security condition not satisfied)"; + if (sw1 == (byte) 0x6A && sw2 == (byte) 0x81) return status + " (Function not supported)"; + if (sw1 == (byte) 0x67 && sw2 == (byte) 0x00) return status + " (Wrong length)"; + if (sw1 == (byte) 0x6D && sw2 == (byte) 0x00) return status + " (Instruction not supported)"; + if (sw1 == (byte) 0x6E && sw2 == (byte) 0x00) return status + " (Class not supported)"; + + return status + " (Error)"; + } + return "Unknown"; + } + + private void updateResult(final String text) { + Log.i("com.example.nfcreader", text); + getActivity().runOnUiThread(() -> resultText.setText(text)); + } + + private byte[] hexToBytes(String s) { + String hex = s.replace(" ", ""); + int len = hex.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + + Character.digit(hex.charAt(i + 1), 16)); + } + return data; + } + + private String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02X", b)); + } + return sb.toString(); + } + + private String hexToString(String hex) { + try { + return new String(hexToBytes(hex), "ASCII"); + } catch (UnsupportedEncodingException e) { + return hex; + } + } + + private String hexToEucKrString(String hex) { + try { + return new String(hexToBytes(hex), "EUC-KR"); + } catch (UnsupportedEncodingException e) { + return hexToString(hex); + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..d393fc7 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_settings.xml b/app/src/main/res/layout/dialog_settings.xml new file mode 100644 index 0000000..188d384 --- /dev/null +++ b/app/src/main/res/layout/dialog_settings.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +