├── app ├── .gitignore ├── src │ ├── main │ │ ├── ic_launcher-playstore.png │ │ ├── res │ │ │ ├── drawable │ │ │ │ ├── nfc_icon_r2.png │ │ │ │ ├── nfc_icon_w2.png │ │ │ │ ├── nfc_icon_20581.png │ │ │ │ ├── ic_outline_home_24.xml │ │ │ │ ├── share_rounded_corner_with_border_error.xml │ │ │ │ ├── share_rounded_corner_with_border_true.xml │ │ │ │ ├── share_rounded_corner_light_blue_with_border_error.xml │ │ │ │ ├── share_rounded_corner_light_green_with_border_error.xml │ │ │ │ ├── share_rounded_corner_light_red_with_border_error.xml │ │ │ │ ├── share_rounded_corner_light_orange_with_border_error.xml │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── raw │ │ │ │ ├── notification_decorative_01.wav │ │ │ │ └── notification_decorative_02.wav │ │ │ ├── values │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── themes.xml │ │ │ │ └── strings.xml │ │ │ ├── layout │ │ │ │ ├── dialog_licenses.xml │ │ │ │ ├── activity_main.xml │ │ │ │ ├── fragment_write.xml │ │ │ │ ├── fragment_home.xml │ │ │ │ └── fragment_read.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ ├── xml │ │ │ │ ├── apduservice.xml │ │ │ │ ├── backup_rules.xml │ │ │ │ └── data_extraction_rules.xml │ │ │ ├── menu │ │ │ │ └── bottom_menu.xml │ │ │ └── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ ├── java │ │ │ └── de │ │ │ │ └── androidcrypto │ │ │ │ └── android_hce_beginner_app │ │ │ │ ├── BasicReader.java │ │ │ │ ├── BasicHceService.java │ │ │ │ ├── LicensesDialogFragment.java │ │ │ │ ├── Utils.java │ │ │ │ ├── MainActivity.java │ │ │ │ ├── WriteFragment.java │ │ │ │ ├── HomeFragment.java │ │ │ │ ├── MyHostApduServiceSimple.java │ │ │ │ ├── KHostApduService.java │ │ │ │ ├── ReadFragment.java │ │ │ │ └── MyHostApduService.java │ │ ├── AndroidManifest.xml │ │ └── assets │ │ │ └── open_source_licenses.html │ ├── test │ │ └── java │ │ │ └── de │ │ │ └── androidcrypto │ │ │ └── android_hce_beginner_app │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── de │ │ └── androidcrypto │ │ └── android_hce_beginner_app │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── .idea ├── .gitignore ├── compiler.xml ├── vcs.xml ├── migrations.xml ├── misc.xml ├── deploymentTargetSelector.xml └── gradle.xml ├── sampleapp ├── README.md └── app-debug.apk ├── screenshots ├── app_home_01.png ├── app_read_01.png ├── app_write_01.png ├── device_settings_01.png ├── device_settings_02.png ├── device_settings_03.png ├── device_settings_04.png ├── device_settings_05.png └── small │ ├── app_home_01.png │ ├── app_read_01.png │ ├── app_write_01.png │ ├── device_settings_01.png │ ├── device_settings_02.png │ ├── device_settings_03.png │ ├── device_settings_04.png │ └── device_settings_05.png ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── .gitignore ├── settings.gradle ├── LICENSE.md ├── gradle.properties ├── gradlew.bat ├── gradlew └── README.md /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /sampleapp/README.md: -------------------------------------------------------------------------------- 1 | This is a ready to use app, compiled in DEBUG mode. 2 | -------------------------------------------------------------------------------- /sampleapp/app-debug.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/sampleapp/app-debug.apk -------------------------------------------------------------------------------- /screenshots/app_home_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/screenshots/app_home_01.png -------------------------------------------------------------------------------- /screenshots/app_read_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/screenshots/app_read_01.png -------------------------------------------------------------------------------- /screenshots/app_write_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/screenshots/app_write_01.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /screenshots/device_settings_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/screenshots/device_settings_01.png -------------------------------------------------------------------------------- /screenshots/device_settings_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/screenshots/device_settings_02.png -------------------------------------------------------------------------------- /screenshots/device_settings_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/screenshots/device_settings_03.png -------------------------------------------------------------------------------- /screenshots/device_settings_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/screenshots/device_settings_04.png -------------------------------------------------------------------------------- /screenshots/device_settings_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/screenshots/device_settings_05.png -------------------------------------------------------------------------------- /screenshots/small/app_home_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/screenshots/small/app_home_01.png -------------------------------------------------------------------------------- /screenshots/small/app_read_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/screenshots/small/app_read_01.png -------------------------------------------------------------------------------- /screenshots/small/app_write_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/screenshots/small/app_write_01.png -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/nfc_icon_r2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/drawable/nfc_icon_r2.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/nfc_icon_w2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/drawable/nfc_icon_w2.png -------------------------------------------------------------------------------- /screenshots/small/device_settings_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/screenshots/small/device_settings_01.png -------------------------------------------------------------------------------- /screenshots/small/device_settings_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/screenshots/small/device_settings_02.png -------------------------------------------------------------------------------- /screenshots/small/device_settings_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/screenshots/small/device_settings_03.png -------------------------------------------------------------------------------- /screenshots/small/device_settings_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/screenshots/small/device_settings_04.png -------------------------------------------------------------------------------- /screenshots/small/device_settings_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/screenshots/small/device_settings_05.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/nfc_icon_20581.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/drawable/nfc_icon_20581.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/raw/notification_decorative_01.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/raw/notification_decorative_01.wav -------------------------------------------------------------------------------- /app/src/main/res/raw/notification_decorative_02.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/raw/notification_decorative_02.wav -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #2196F3 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidCrypto/Android_HCE_Beginner_App/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_licenses.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Sep 03 17:44:03 CEST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /.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 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/de/androidcrypto/android_hce_beginner_app/BasicReader.java: -------------------------------------------------------------------------------- 1 | package de.androidcrypto.android_hce_beginner_app; 2 | 3 | import android.nfc.NfcAdapter; 4 | import android.nfc.Tag; 5 | 6 | public class BasicReader implements NfcAdapter.ReaderCallback { 7 | @Override 8 | public void onTagDiscovered(Tag tag) { 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_outline_home_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/share_rounded_corner_with_border_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/share_rounded_corner_with_border_true.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/share_rounded_corner_light_blue_with_border_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/share_rounded_corner_light_green_with_border_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/share_rounded_corner_light_red_with_border_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/share_rounded_corner_light_orange_with_border_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF000000 4 | #FFFFFFFF 5 | #FF03DAC5 6 | #FF018786 7 | 8 | #2196F3 9 | #1E88E5 10 | #3F51B5 11 | #8E24AA 12 | -------------------------------------------------------------------------------- /app/src/main/res/xml/apduservice.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/test/java/de/androidcrypto/android_hce_beginner_app/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package de.androidcrypto.android_hce_beginner_app; 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 | } -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/menu/bottom_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 13 | 14 | 18 | 19 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google { 4 | content { 5 | includeGroupByRegex("com\\.android.*") 6 | includeGroupByRegex("com\\.google.*") 7 | includeGroupByRegex("androidx.*") 8 | } 9 | } 10 | mavenCentral() 11 | gradlePluginPortal() 12 | } 13 | } 14 | dependencyResolutionManagement { 15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 16 | repositories { 17 | google() 18 | mavenCentral() 19 | } 20 | } 21 | 22 | rootProject.name = "Android_HCE_Beginner_App" 23 | include ':app' 24 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/de/androidcrypto/android_hce_beginner_app/BasicHceService.java: -------------------------------------------------------------------------------- 1 | package de.androidcrypto.android_hce_beginner_app; 2 | 3 | import static de.androidcrypto.android_hce_beginner_app.MyHostApduServiceSimple.byteArrayToHexString; 4 | import android.nfc.cardemulation.HostApduService; 5 | import android.os.Bundle; 6 | import android.util.Log; 7 | 8 | public class BasicHceService extends HostApduService { 9 | private static final String TAG = "HCE"; 10 | 11 | @Override 12 | public byte[] processCommandApdu(byte[] commandApdu, Bundle bundle) { 13 | Log.i(TAG, "Received APDU: " + byteArrayToHexString(commandApdu)); 14 | return new byte[0]; 15 | } 16 | 17 | @Override 18 | public void onDeactivated(int i) { 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/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 -------------------------------------------------------------------------------- /app/src/androidTest/java/de/androidcrypto/android_hce_beginner_app/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package de.androidcrypto.android_hce_beginner_app; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("de.androidcrypto.android_hce_beginner_app", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.6.0" 3 | junit = "4.13.2" 4 | junitVersion = "1.2.1" 5 | espressoCore = "3.6.1" 6 | appcompat = "1.7.0" 7 | material = "1.12.0" 8 | activity = "1.9.1" 9 | constraintlayout = "2.1.4" 10 | 11 | [libraries] 12 | junit = { group = "junit", name = "junit", version.ref = "junit" } 13 | ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } 14 | espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } 15 | appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } 16 | material = { group = "com.google.android.material", name = "material", version.ref = "material" } 17 | activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } 18 | constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } 19 | 20 | [plugins] 21 | android-application = { id = "com.android.application", version.ref = "agp" } 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 15 | open_source_licenses.html 16 | 17 | 18 |

19 | Personal Use Only 20 |

21 | 30 | 31 |

32 | Creative Commons 33 |
34 | Attribution 4.0 International (CC BY 4.0) 35 |

36 | 43 |
 44 | Sound files licensed under Attribution 4.0 International (CC BY 4.0):
 45 | https://creativecommons.org/licenses/by/4.0/
 46 | 
 47 | Creative Commons Attribution 4.0 International Public License
 48 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
 49 | 
 50 | Section 1 – Definitions.
 51 | 
 52 | Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
 53 | Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
 54 | Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
 55 | Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
 56 | Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
 57 | Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
 58 | Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
 59 | Licensor means the individual(s) or entity(ies) granting rights under this Public License.
 60 | Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
 61 | Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
 62 | You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.
 63 | Section 2 – Scope.
 64 | 
 65 | License grant.
 66 | Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
 67 | reproduce and Share the Licensed Material, in whole or in part; and
 68 | produce, reproduce, and Share Adapted Material.
 69 | Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
 70 | Term. The term of this Public License is specified in Section 6(a).
 71 | Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
 72 | Downstream recipients.
 73 | Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
 74 | No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
 75 | No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
 76 | Other rights.
 77 | 
 78 | Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
 79 | Patent and trademark rights are not licensed under this Public License.
 80 | To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties.
 81 | Section 3 – License Conditions.
 82 | 
 83 | Your exercise of the Licensed Rights is expressly made subject to the following conditions.
 84 | 
 85 | Attribution.
 86 | 
 87 | If You Share the Licensed Material (including in modified form), You must:
 88 | 
 89 | retain the following if it is supplied by the Licensor with the Licensed Material:
 90 | identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
 91 | a copyright notice;
 92 | a notice that refers to this Public License;
 93 | a notice that refers to the disclaimer of warranties;
 94 | a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
 95 | indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
 96 | indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
 97 | You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
 98 | If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
 99 | If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License.
100 | Section 4 – Sui Generis Database Rights.
101 | 
102 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
103 | 
104 | for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database;
105 | if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and
106 | You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
107 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
108 | Section 5 – Disclaimer of Warranties and Limitation of Liability.
109 | 
110 | Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.
111 | To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.
112 | The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
113 | Section 6 – Term and Termination.
114 | 
115 | This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
116 | Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
117 | 
118 | automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
119 | upon express reinstatement by the Licensor.
120 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
121 | For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
122 | Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
123 | Section 7 – Other Terms and Conditions.
124 | 
125 | The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
126 | Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
127 | Section 8 – Interpretation.
128 | 
129 | For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
130 | To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
131 | No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
132 | Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
133 | Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses.
134 | 
135 | Creative Commons may be contacted at creativecommons.org.
136 | 
137 | 138 | 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android HCE Beginner App 2 | 3 | ## Host-based card emulation overview 4 | 5 | I will quote a lot of times from this official document, see the full document here: 6 | https://developer.android.com/develop/connectivity/nfc/hce 7 | 8 | *Android 4.4 and higher provide an additional method of **card emulation** that doesn't involve a secure element, 9 | called **host-based card emulation**. This allows any Android application to emulate a card and talk directly 10 | to the NFC reader. This topic describes how host-based card emulation (**HCE**) works on Android and how you 11 | can develop an app that emulates an NFC card using this technique.* 12 | 13 | ## Purpose of this App 14 | 15 | This app explains how to setup an application that runs the **Host-Based Card Emulation (HCE)** feature on an 16 | Android device. I'm describing step by step what requirements are to meet to transfer some very simple data. 17 | **There will be no special features like emulating a complete NFC card**, but in the end you will understand what the 18 | steps are to write your own app. 19 | 20 | This repository is accompanied by a tutorial on medium.com, please find the article here: https://medium.com/@androidcrypto/how-to-use-host-based-card-emulation-hce-in-android-a-beginner-tutorial-java-32974dd89529 21 | 22 | ## Requirements to follow the tutorial 23 | 24 | As our Android device will act as a real NFC tag, you will need a **second NFC-Reader** (e.g. a second Android device) to run the tests, 25 | because when an Android device is in HCE mode it cannot read "itself" (on the same device). Unfortunately, not all Android devices, 26 | even with an NFC reader, are enabled for HCE, so you need to check that your device is capable to run HCE. You can run a simple check 27 | using "**FEATURE_NFC_HOST_CARD_EMULATION**" to verify that you can run HCE. 28 | 29 | The minimum SDK version for this app is Android 5 (SDK 21), and your device needs to have an NFC reader on board. 30 | 31 | This app is developed using Android Studio 2024.1.1 and I'm using the Java Runtime version: 17.0.11+0-17.0.11b1207.24-11852314 aarch64 32 | on MacOS/Silicon. 33 | 34 | ## Most important facts 35 | 36 | ### Application Identifier (AID) 37 | 38 | I'm receiving a lot of questions like *"why does my HCE implementation does not work and is not recognized by another device ?"*. 39 | In most of the cases the reason for the failure is that both devices use a different **Application Identifier" ("**AID**"). In the 40 | end, that means they speak "different languages" and don't understand each other. 41 | 42 | From Wikipedia: *An application identifier (AID) is used to address an application in the card or Host Card Emulation (HCE) if 43 | delivered without a card. An AID consists of a registered application provider identifier (RID) of five bytes, which is issued 44 | by the ISO/IEC 7816-5 registration authority. This is followed by a proprietary application identifier extension (PIX), which 45 | enables the application provider to differentiate among the different applications offered. The AID is printed on all EMV 46 | cardholder receipts. Card issuers can alter the application name from the name of the card network.* 47 | 48 | **Registered Application Identifier**: You cannot choose an Application identifier (AID) on your own because then you may get 49 | trouble when working with established reader infrastructure. There are three groups of AIDs: 50 | - Category "A": That are international registered AIDs, mainly for Payment Provider like Mastercard or Visa Card. Please don't choose an AID from this category ! 51 | - Category "D": That are national registered AIDs, e.g. for Payment Provider like local bank cards or for accessing NFC tags. Please don't choose an AID from this category ! 52 | - Category "F": That are proprietary (non registered) AIDs, e.g. for this application. 53 | 54 | Please find a very extensive list of existing AID's for payment cards here: https://www.eftlab.com/knowledge-base/complete-list-of-application-identifiers-aid 55 | 56 | Please do not use AIDs with a length less than 5 as they won't get routed on Android devices properly. The maximum length of 57 | an AID is 16 bytes (see https://stackoverflow.com/a/27620724/8166854 for more details). 58 | 59 | This app uses the AID **F22334455667**. 60 | 61 | ### Run a HCE services in the background 62 | 63 | Most people believe, that the emulated tag is run by your app, but that is not true. Of course, you need to run an app for the first 64 | time to start a (HCE) service in the background, but from now on all communication is done **between your service and the remote NFC reader**. 65 | Your app will get no further updates about the communication or and information that data is exchanged. You need to implement 66 | an interface to get informed about any updates. For that reason it not very easy to monitor your running app from "inside" your app. 67 | 68 | Your app will still persistent to work, even when you close your app with the Android Taskmanager - because it is a service. 69 | 70 | ### Payment and other applications 71 | 72 | If you are running the Google Wallet app on your device you know that your device is HCE capable, because Google Wallet emulates a 73 | **Payment Card** (like Credit Cards). Those applications are called **Payment applications** and usually a device should run only one payment 74 | application for emulating. For that reason we will design our app as "other application" HCE type to avoid any conflicts with existing 75 | payment apps. 76 | 77 | ### HCE and security 78 | 79 | From the documentation: *The HCE architecture provides one core piece of security: because your service is protected by the **BIND_NFC_SERVICE** 80 | system permission, only the OS can bind to and communicate with your service. This ensures that any APDU you receive is actually an APDU that 81 | was received by the OS from the NFC controller, and that any APDU you send back only goes to the OS, which in turn directly forwards the APDUs 82 | to the NFC controller.* 83 | 84 | *The last remaining concern is where you get your data that your app sends to the NFC reader. This is intentionally decoupled in the HCE design; 85 | it does not care where the data comes from, it just makes sure that it is safely transported to the NFC controller and out to the NFC reader.* 86 | 87 | *For securely storing and retrieving the data that you want to send from your HCE service, you can, for example, rely on the Android Application 88 | Sandbox, which isolates your app's data from other apps. For more details about Android security, read Security tips 89 | (https://developer.android.com/training/articles/security-tips).* 90 | 91 | ### What is the application protocol data unit (APDU) ? 92 | 93 | You will notice that "**APDU**" is written in many tutorials and source codes. The APDU protocol is the way how an NFC-Reader and NFC-Smart Card are 94 | going to communicate, it is an **APDU message command-response pair**, consisting of two pieces: 95 | 96 | a) The NFC-Reader is sending data by sending a **command APDU** to the NFC Smart Card 97 | 98 | b) The NFC Smart Card answers the command by sending a **response APDU** to the NFC Reader. 99 | 100 | In our case, our HCE app needs to response to a command APDU as our app is acting like a real NFC Smart Card. As each NFC Smartcard follows a card 101 | specific protocol it is important that our HCE app acts like the same. If not - the real NFC Reader will think "that is not the NFC tag I expected" 102 | and will stop any further communication with out app. 103 | 104 | ### ISO 7816-4 Section 6 - Basic Interindustry Commands 105 | 106 | To understand the commands between an NFC reader and a tag (real or emulated one) you should get familiar with the ISO 7816-4 commands. 107 | In this tutorial I'm using just 3 of them: 108 | 109 | **SELECT APPLICATION APDU**: The "Select Application" APDU is the starting point for every communication with an emulated (HCE driven) NFC tag. 110 | If you miss to send this command your HCE tag won't recognize the request and will not answer or react in any way. The second fact is: as I'm 111 | using a proprietary AID for this app an NFC reader like the TagInfo app by NXP does not know about this specific AID you cannot read the HCE 112 | tag with regular NFC reader apps. The "Select Application APDU" for the HCE emulated tag of the Beginner App is "00A4040006F2233445566700h". 113 | To understand the response see the chapter APDU Responses. 114 | 115 | **GET DATA APDU**: The "Get Data" APDU is used to request data from the tag. In most more complex NFC tags the data is organized within files 116 | like the one you know from your home computer. The file system is organized by using file numbers that are part of the command: 117 | 118 | ```vplaintext 119 | Get Data APDU: 00ca0000010100 120 | I'm splitting the bytes into their meaning (all data are hex encoded): 121 | 122 | 00: CLA = Class of instruction, length 1 byte, the start of the command sequence 123 | CA: INS = Instruction, length 1 byte, is the GET DATA command 124 | 00: P 1 = Selection Control 1, length 1 byte, in range '0000'-'003F' it is RFU 125 | 00: P 2 = Selection Control 2, length 1 byte, in range '0000'-'003F' it is RFU 126 | 01: LC Field: Length of the following data, length 1 byte, number of following data bytes 127 | 01: Data: Data send together with the command, here the 1 byte long file number 128 | 00: Le field: Empty or maximum length of data expected in response 129 | ``` 130 | The "Get Data" APDU for reading the content of file 01 from the HCE emulated tag of the Beginner App is "00ca0000010100h". To understand 131 | the response see the chapter APDU Responses. 132 | 133 | **PUT DATA APDU**: The "Put Data" APDU is used to send data to the tag. In most more complex NFC tags the data is organized within files 134 | like the one you know from your home computer. The file system is organized by using file numbers that are part of the command, together 135 | with data that gets stored: 136 | 137 | ```plaintext 138 | PUT DATA APDU: 00da00001d024e65..303200 139 | I'm splitting the bytes into their meaning (all data are hex encoded): 140 | 141 | 00: CLA = Class of instruction, length 1 byte, the start of the command sequence 142 | DA: INS = Instruction, length 1 byte, is the PUT DATA command 143 | 00: P 1 = Selection Control 1, length 1 byte, in range '0000'-'003F' it is RFU 144 | 00: P 2 = Selection Control 2, length 1 byte, in range '0000'-'003F' it is RFU 145 | 1d: LC Field: Length of the following data, length 1 byte, number of following data bytes 146 | 1dh = 29 bytes of data following 147 | 01: Data: Data send together with the command, here the 1 byte long file number (here file 02) 148 | 4e65..3032: Data: Data send together with the command, here the 28 bytes long data 149 | 00: Le field: Empty or maximum length of data expected in response 150 | ``` 151 | 152 | In this command the data consists of 2 single data fields the 1 byte long file number and the data (here 28 bytes). I'm sending this string: 153 | 154 | ```plaintext 155 | dataToWrite in UTF-8 encoding: New Content in fileNumber 02 156 | dataToWrite in hex encoding: 4e657720436f6e74656e7420696e2066696c654e756d626572203032 157 | ``` 158 | The "PUT DATA" APDU for the HCE emulated tag of the Beginner App is "00da00001d024e657720436f6e74656e7420696e2066696c654e756d62657220303200h". 159 | To understand the response see the chapter APDU Responses. 160 | 161 | **APDU Responses**: To understand the APDU response we need to divide all commands in one of the two categories: 162 | 163 | - send the command only or send the command together with data (e.g. PUT DATA APDU) 164 | - send the command (together with specifying parameter like the file number) and receive data in return (e.g. GET DATA APDU). 165 | 166 | - Send the command only or with data: If the card accepts the command it simply answers with a 2 bytes long code: "9000h": 167 | 168 | ```plaintext 169 | OK-Response: 9000h 170 | 90 = SW 1 = Status byte 1 = Command processing status 171 | 00 = SW 2 = Status byte 2 = Command processing qualifier 172 | ``` 173 | 174 | If the bytes are "9000h" the command was "Accepted". Every other response should be treated as "Not Accepted". The ISO can define a more 175 | specified response for different cases, but in our case we just check for "9000h" or not "9000h". 176 | 177 | - Send the command and receive data in return: In this case the returned data is a little bit different. In case of command acceptance we 178 | - receive the data, concatenated with the two Status bytes. This is the sample response to the "Get Data" APDU: 179 | 180 | ```plaintext 181 | Get Data APDU: 00ca0000010100 182 | Response: 48434520426567696e6e65722041707020319000 183 | The complete response is 20 bytes long: 184 | Data (18 bytes): 48434520426567696e6e6572204170702031 185 | Status Bytes 1+2: 9000 186 | The Data is this String: HCE Beginner App 1 187 | ``` 188 | 189 | In case of success we get the data, followed by the 2 bytes long Status bytes. In case of failure (e.g. wrong command length) the tag answers 190 | with "0000h". The Beginner app knows a third response case: in case the file number does not exist (e.g. file number 3) the tag is returning 191 | the string "HCE Beginner App Unknown", followed by the OK-Status bytes "9000h". Every other response should be treated as "Not Accepted". The 192 | ISO can define a more specified response for different cases, but in our case we just check for "9000h" or not "9000h". 193 | 194 | For more details about ABDU's see: https://cardwerk.com/smart-card-standard-iso7816-4-section-6-basic-interindustry-commands/ 195 | 196 | PUT DATA command 197 | ```plaintext 198 | PUT DATA command APDU 199 | CLA 00h 200 | INS DAh 201 | P1 00h 202 | P2 00h 203 | Lc field Length of the subsequent data field 204 | Data field Parameters and data to be written 205 | Le field Empty 206 | ``` 207 | 208 | ## UID of an emulated NFC tag 209 | 210 | If you run my app you will notice that I'm exposing the tag UID in the Logfile within the Reader app. This 211 | **UID is exact 4 bytes long and random**. This is a security feature to prevent privacy to the user - this 212 | way a large NFC reader infrastructure (think of a "Smart City" with hundreds of NFC readers) is not being 213 | able to track a user (better: his NFC card). There is no workaround to change the behaviour of a modern 214 | Android device to get a static UID each time you tap the emulated tag to an NFC reader, sorry. 215 | 216 | The only way would be to store the UID in a "read only" file on the tag to get a unambiguously tag identification. 217 | 218 | ## Steps to create a HCE application 219 | 220 | These are the basic steps you need to implement a HCE application on your device: 221 | 222 | 1) Create a HCE service class that is extending the **HostApduService** 223 | 2) Register your HCE service in **AndroidManifest.xml** 224 | 3) Create an **XML-file in your resources** that defines the application identifier your HCE application in working on ("apduservice.xml") 225 | 4) Register the XML-file in **AndroidManifest.xml** to link the AID with your own HCE service 226 | 227 | **That's all ?** - yes we don't need more steps to build a HCE application. 228 | 229 | ## Example output 230 | 231 | ```plaintext 232 | TagId: 08ca20a8 233 | selectApdu with AID: 00a4040006f2233445566700 234 | selectApdu response: 0000 235 | response length: 2 data: 0000 236 | getDataApdu with file01: 00ca0000010100 237 | response length: 20 data: 48434520426567696e6e65722041707020319000 238 | HCE Beginner App 1 239 | getDataApdu with file02: 00ca0000010200 240 | response length: 20 data: 48434520426567696e6e65722041707020329000 241 | HCE Beginner App 2 242 | putDataApdu with file02: 00da00001d024e657720436f6e74656e7420696e2066696c654e756d62657220303200 243 | response length: 2 data: 9000 244 | SUCCESS 245 | getDataApdu with file02: 00ca0000010200 246 | response length: 30 data: 4e657720436f6e74656e7420696e2066696c654e756d6265722030329000 247 | New Content in fileNumber 02 248 | ``` 249 | 250 | ## Some data regarding the reader application 251 | 252 | The NFC reader application is designed to communicate with the HCE emulated tag. It will probably not work 253 | with other (real or emulated) NFC tags as it uses a static, proprietary Application Identifier ("F22334455667"). 254 | 255 | The reader is following this workflow: 256 | 257 | - connect to the HCE tag by sending the **Select Application APDU** command 258 | - send the **Get Data APDU** command to retrieve the content of file number 01 on the HCE tag 259 | - send the **Get Data APDU** command to retrieve the content of file number 02 on the HCE tag 260 | - send the **Put Data APDU** command to write a new string on file number 02 on the HCE tag 261 | - send the **Get Data APDU** command to retrieve the new content of file number 02 on the HCE tag 262 | - EOC - End of Communication 263 | 264 | Latest update: Sep. 18.th, 2024 265 | 266 | ## License 267 | 268 | Android HCE Beginner Appis available under the MIT license. See the LICENSE.md file for more info. 269 | -------------------------------------------------------------------------------- /app/src/main/java/de/androidcrypto/android_hce_beginner_app/ReadFragment.java: -------------------------------------------------------------------------------- 1 | package de.androidcrypto.android_hce_beginner_app; 2 | 3 | import static de.androidcrypto.android_hce_beginner_app.Utils.bytesToHexNpe; 4 | import static de.androidcrypto.android_hce_beginner_app.Utils.doVibrate; 5 | import static de.androidcrypto.android_hce_beginner_app.Utils.hexStringToByteArray; 6 | 7 | import android.content.ComponentName; 8 | import android.content.Intent; 9 | import android.content.pm.PackageManager; 10 | import android.graphics.Color; 11 | import android.media.MediaPlayer; 12 | import android.nfc.NfcAdapter; 13 | import android.nfc.Tag; 14 | import android.nfc.cardemulation.CardEmulation; 15 | import android.nfc.tech.IsoDep; 16 | import android.nfc.tech.NfcA; 17 | import android.os.Bundle; 18 | import android.provider.Settings; 19 | import android.text.Spannable; 20 | import android.text.SpannableString; 21 | import android.text.style.BackgroundColorSpan; 22 | import android.util.Log; 23 | import android.view.LayoutInflater; 24 | import android.view.View; 25 | import android.view.ViewGroup; 26 | import android.widget.RadioButton; 27 | import android.widget.TextView; 28 | import android.widget.Toast; 29 | 30 | import androidx.annotation.NonNull; 31 | import androidx.annotation.Nullable; 32 | import androidx.fragment.app.Fragment; 33 | 34 | import java.io.IOException; 35 | import java.nio.charset.StandardCharsets; 36 | import java.util.Arrays; 37 | 38 | /** 39 | * A simple {@link Fragment} subclass. 40 | * Use the {@link ReadFragment#newInstance} factory method to 41 | * create an instance of this fragment. 42 | */ 43 | public class ReadFragment extends Fragment implements NfcAdapter.ReaderCallback { 44 | 45 | // TODO: Rename parameter arguments, choose names that match 46 | // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER 47 | private static final String ARG_PARAM1 = "param1"; 48 | private static final String ARG_PARAM2 = "param2"; 49 | private static final String TAG = "ReadFragment"; 50 | 51 | // TODO: Rename and change types of parameters 52 | private String mParam1; 53 | private String mParam2; 54 | 55 | public ReadFragment() { 56 | // Required empty public constructor 57 | } 58 | 59 | /** 60 | * Use this factory method to create a new instance of 61 | * this fragment using the provided parameters. 62 | * 63 | * @param param1 Parameter 1. 64 | * @param param2 Parameter 2. 65 | * @return A new instance of fragment ReceiveFragment. 66 | */ 67 | // TODO: Rename and change types and number of parameters 68 | public static ReadFragment newInstance(String param1, String param2) { 69 | ReadFragment fragment = new ReadFragment(); 70 | Bundle args = new Bundle(); 71 | args.putString(ARG_PARAM1, param1); 72 | args.putString(ARG_PARAM2, param2); 73 | fragment.setArguments(args); 74 | return fragment; 75 | } 76 | 77 | private TextView readResult; 78 | private View loadingLayout; 79 | private String outputString = ""; // used for the UI output 80 | private NfcAdapter mNfcAdapter; 81 | 82 | @Override 83 | public void onCreate(Bundle savedInstanceState) { 84 | super.onCreate(savedInstanceState); 85 | if (getArguments() != null) { 86 | mParam1 = getArguments().getString(ARG_PARAM1); 87 | mParam2 = getArguments().getString(ARG_PARAM2); 88 | } 89 | mNfcAdapter = NfcAdapter.getDefaultAdapter(this.getContext()); 90 | } 91 | 92 | @Override 93 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 94 | readResult = getView().findViewById(R.id.tvReadResult); 95 | loadingLayout = getView().findViewById(R.id.loading_layout); 96 | } 97 | 98 | @Override 99 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 100 | Bundle savedInstanceState) { 101 | // Inflate the layout for this fragment 102 | return inflater.inflate(R.layout.fragment_read, container, false); 103 | } 104 | 105 | // This method is running in another thread when a card is discovered 106 | // This method cannot cannot direct interact with the UI Thread 107 | // Use `runOnUiThread` method to change the UI from this method 108 | @Override 109 | public void onTagDiscovered(Tag tag) { 110 | System.out.println("NFC tag discovered"); 111 | playSinglePing(); 112 | 113 | setLoadingLayoutVisibility(true); 114 | outputString = ""; 115 | 116 | requireActivity().runOnUiThread(() -> { 117 | readResult.setBackgroundColor(getResources().getColor(R.color.white)); 118 | readResult.setText(""); 119 | }); 120 | 121 | IsoDep isoDep = IsoDep.get(tag); 122 | if (isoDep == null) { 123 | Log.e(TAG, "isoDep is NULL, aborted"); 124 | writeToUiAppend("The tag is not readable with IsoDep class, sorry"); 125 | writeToUiFinal(readResult); 126 | setLoadingLayoutVisibility(false); 127 | returnOnNotSuccess(); 128 | return; 129 | } else { 130 | Log.i(TAG, "isoDep is available"); 131 | } 132 | 133 | byte[] tagId = isoDep.getTag().getId(); 134 | writeToUiAppend("TagId: " + bytesToHexNpe(tagId)); 135 | 136 | try { 137 | isoDep.connect(); 138 | byte[] command, response; 139 | 140 | String aidString = "F22334455667"; 141 | byte[] aid = Utils.hexStringToByteArray(aidString); 142 | command = selectApdu(aid); 143 | response = isoDep.transceive(command); 144 | writeToUiAppend("selectApdu with AID: " + bytesToHexNpe(command)); 145 | if (response == null) { 146 | writeToUiAppend("selectApdu with AID fails (null), aborted"); 147 | return; 148 | } else { 149 | writeToUiAppend("response length: " + response.length + " data: " + bytesToHexNpe(response)); 150 | Log.i(TAG, "response: " + bytesToHexNpe(response)); 151 | } 152 | 153 | // asking for data in file 01 154 | int fileNumber01 = 1; 155 | command = getDataApdu(fileNumber01); 156 | response = isoDep.transceive(command); 157 | writeToUiAppend("getDataApdu with file01: " + bytesToHexNpe(command)); 158 | if (response == null) { 159 | writeToUiAppend("getDataApdu with file01 fails (null)"); 160 | } else { 161 | writeToUiAppend("response length: " + response.length + " data: " + bytesToHexNpe(response)); 162 | } 163 | // verify response 164 | if (checkResponse(response)) { 165 | writeToUiAppend(new String(returnDataBytes(response), StandardCharsets.UTF_8)); 166 | Log.i(TAG, "response: " + bytesToHexNpe(returnDataBytes(response))); 167 | } else { 168 | writeToUiAppend("The tag returned NOT OK"); 169 | Log.i(TAG, "The tag returned NOT OK"); 170 | } 171 | 172 | // read previous content of file 02 173 | int fileNumber02 = 2; 174 | command = getDataApdu(fileNumber02); 175 | response = isoDep.transceive(command); 176 | writeToUiAppend("getDataApdu with file02: " + bytesToHexNpe(command)); 177 | if (response == null) { 178 | writeToUiAppend("getDataApdu with file02 fails (null)"); 179 | } else { 180 | writeToUiAppend("response length: " + response.length + " data: " + bytesToHexNpe(response)); 181 | } 182 | // verify response 183 | if (checkResponse(response)) { 184 | writeToUiAppend(new String(returnDataBytes(response), StandardCharsets.UTF_8)); 185 | Log.i(TAG, "response: " + bytesToHexNpe(returnDataBytes(response))); 186 | } else { 187 | writeToUiAppend("The tag returned NOT OK"); 188 | Log.i(TAG, "The tag returned NOT OK"); 189 | } 190 | 191 | // write data to fileNumber 02 192 | byte[] dataToWrite = "New Content in fileNumber 02".getBytes(StandardCharsets.UTF_8); 193 | command = putDataApdu(fileNumber02, dataToWrite); 194 | response = isoDep.transceive(command); 195 | writeToUiAppend("putDataApdu with file02: " + bytesToHexNpe(command)); 196 | if (response == null) { 197 | writeToUiAppend("putDataApdu with file02 fails (null)"); 198 | } else { 199 | writeToUiAppend("response length: " + response.length + " data: " + bytesToHexNpe(response)); 200 | } 201 | // verify response 202 | if (checkResponse(response)) { 203 | writeToUiAppend("SUCCESS"); 204 | Log.i(TAG, "response: " + bytesToHexNpe(returnDataBytes(response))); 205 | } else { 206 | writeToUiAppend("The tag returned NOT OK"); 207 | Log.i(TAG, "The tag returned NOT OK"); 208 | } 209 | 210 | // read updated content 211 | command = getDataApdu(fileNumber02); 212 | response = isoDep.transceive(command); 213 | writeToUiAppend("getDataApdu with file02: " + bytesToHexNpe(command)); 214 | if (response == null) { 215 | writeToUiAppend("getDataApdu with file02 fails (null)"); 216 | } else { 217 | writeToUiAppend("response length: " + response.length + " data: " + bytesToHexNpe(response)); 218 | } 219 | // verify response 220 | if (checkResponse(response)) { 221 | writeToUiAppend(new String(returnDataBytes(response), StandardCharsets.UTF_8)); 222 | Log.i(TAG, "response: " + bytesToHexNpe(returnDataBytes(response))); 223 | } else { 224 | writeToUiAppend("The tag returned NOT OK"); 225 | Log.i(TAG, "The tag returned NOT OK"); 226 | } 227 | 228 | isoDep.close(); 229 | } catch (IOException e) { 230 | writeToUiAppend("IOException on connection: " + e.getMessage()); 231 | Log.e(TAG, "IOException on connection: " + e.getMessage()); 232 | e.printStackTrace(); 233 | } catch (Exception e) { 234 | writeToUiAppend("Exception on connection: " + e.getMessage()); 235 | Log.e(TAG, "Exception on connection: " + e.getMessage()); 236 | e.printStackTrace(); 237 | } 238 | 239 | writeToUiFinal(readResult); 240 | playDoublePing(); 241 | setLoadingLayoutVisibility(false); 242 | doVibrate(getActivity()); 243 | } 244 | 245 | private void returnOnNotSuccess() { 246 | writeToUiAppend("=== Return on Not Success ==="); 247 | writeToUiFinal(readResult); 248 | playDoublePing(); 249 | setLoadingLayoutVisibility(false); 250 | doVibrate(getActivity()); 251 | mNfcAdapter.disableReaderMode(this.getActivity()); 252 | } 253 | 254 | private byte[] selectApdu(byte[] aid) { 255 | byte[] commandApdu = new byte[6 + aid.length]; 256 | commandApdu[0] = (byte) 0x00; // CLA 257 | commandApdu[1] = (byte) 0xA4; // INS 258 | commandApdu[2] = (byte) 0x04; // P1 259 | commandApdu[3] = (byte) 0x00; // P2 260 | commandApdu[4] = (byte) (aid.length & 0x0FF); // Lc 261 | System.arraycopy(aid, 0, commandApdu, 5, aid.length); 262 | commandApdu[commandApdu.length - 1] = (byte) 0x00; // Le 263 | return commandApdu; 264 | } 265 | 266 | /** 267 | * getDataApdu is asking for data in file 268 | * @param file is the identifier on the (emulated) tag 269 | * @return 270 | */ 271 | private byte[] getDataApdu(byte[] file) { 272 | byte[] commandApdu = new byte[6 + file.length]; 273 | commandApdu[0] = (byte) 0x00; // CLA 274 | commandApdu[1] = (byte) 0xCA; // INS 275 | commandApdu[2] = (byte) 0x00; // P1 276 | commandApdu[3] = (byte) 0x00; // P2 277 | commandApdu[4] = (byte) (file.length & 0x0FF); // Lc 278 | System.arraycopy(file, 0, commandApdu, 5, file.length); 279 | commandApdu[commandApdu.length - 1] = (byte) 0x00; // Le 280 | return commandApdu; 281 | } 282 | 283 | private byte[] getDataApdu(int file) { 284 | byte[] commandApdu = new byte[6 + 1]; // 6 + byte length 285 | commandApdu[0] = (byte) 0x00; // CLA 286 | commandApdu[1] = (byte) 0xCA; // INS 287 | commandApdu[2] = (byte) 0x00; // P1 288 | commandApdu[3] = (byte) 0x00; // P2 289 | commandApdu[4] = (byte) 0x01; // Lc 290 | commandApdu[5] = (byte) (file & 0x0FF); 291 | commandApdu[commandApdu.length - 1] = (byte) 0x00; // Le 292 | return commandApdu; 293 | } 294 | 295 | private byte[] putDataApdu(int fileNumber, byte[] dataToWrite) { 296 | byte[] commandApdu = new byte[6 + 1 + dataToWrite.length]; // 6 + fileNumber + dataToWrite 297 | commandApdu[0] = (byte) 0x00; // CLA 298 | commandApdu[1] = (byte) 0xDA; // INS 299 | commandApdu[2] = (byte) 0x00; // P1 300 | commandApdu[3] = (byte) 0x00; // P2 301 | commandApdu[4] = (byte) ((dataToWrite.length + 1) & 0x0FF); // Lc 302 | commandApdu[5] = (byte) (fileNumber & 0x0FF); // file number 303 | System.arraycopy(dataToWrite, 0, commandApdu, 6, dataToWrite.length); // dataToWrite 304 | commandApdu[commandApdu.length - 1] = (byte) 0x00; // Le 305 | return commandApdu; 306 | } 307 | 308 | /** 309 | * checks if the response has an 0x'9000' at the end means success 310 | * and the method returns true. 311 | * if any other trailing bytes show up the method returns false 312 | * 313 | * @param data 314 | * @return 315 | */ 316 | private boolean checkResponse(@NonNull byte[] data) { 317 | // simple sanity check 318 | if (data.length < 2) { 319 | return false; 320 | } // not ok 321 | int status = ((0xff & data[data.length - 2]) << 8) | (0xff & data[data.length - 1]); 322 | if (status == 0x9000) { 323 | return true; 324 | } else { 325 | return false; 326 | } 327 | } 328 | 329 | /** 330 | * Return the data without the attached status bytes 331 | * @param data 332 | * @return 333 | */ 334 | private byte[] returnDataBytes(byte[] data) { 335 | if (data == null) return null; 336 | if (data.length < 3) return null; 337 | return Arrays.copyOfRange(data, 0, (data.length - 2)); 338 | } 339 | 340 | /** 341 | * Sound files downloaded from Material Design Sounds 342 | * https://m2.material.io/design/sound/sound-resources.html 343 | */ 344 | private void playSinglePing() { 345 | MediaPlayer mp = MediaPlayer.create(getContext(), R.raw.notification_decorative_02); 346 | mp.start(); 347 | } 348 | 349 | private void playDoublePing() { 350 | MediaPlayer mp = MediaPlayer.create(getContext(), R.raw.notification_decorative_01); 351 | mp.start(); 352 | } 353 | 354 | private void writeToUiAppend(String message) { 355 | //System.out.println(message); 356 | outputString = outputString + message + "\n"; 357 | } 358 | 359 | private void writeToUiFinal(final TextView textView) { 360 | if (textView == (TextView) readResult) { 361 | getActivity().runOnUiThread(new Runnable() { 362 | @Override 363 | public void run() { 364 | textView.setText(outputString); 365 | System.out.println(outputString); // print the data to console 366 | } 367 | }); 368 | } 369 | } 370 | 371 | /** 372 | * shows a progress bar as long as the reading lasts 373 | * 374 | * @param isVisible 375 | */ 376 | 377 | private void setLoadingLayoutVisibility(boolean isVisible) { 378 | getActivity().runOnUiThread(() -> { 379 | if (isVisible) { 380 | loadingLayout.setVisibility(View.VISIBLE); 381 | } else { 382 | loadingLayout.setVisibility(View.GONE); 383 | } 384 | }); 385 | } 386 | 387 | private void showWirelessSettings() { 388 | Toast.makeText(this.getContext(), "You need to enable NFC", Toast.LENGTH_SHORT).show(); 389 | Intent intent = new Intent(Settings.ACTION_WIRELESS_SETTINGS); 390 | startActivity(intent); 391 | } 392 | 393 | @Override 394 | public void onResume() { 395 | super.onResume(); 396 | 397 | if (mNfcAdapter != null) { 398 | 399 | if (!mNfcAdapter.isEnabled()) 400 | showWirelessSettings(); 401 | 402 | Bundle options = new Bundle(); 403 | // Work around for some broken Nfc firmware implementations that poll the card too fast 404 | options.putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250); 405 | 406 | // Enable ReaderMode for NfcA types of card and disable platform sounds 407 | // the option NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK is NOT set 408 | // to get the data of the tag after reading 409 | mNfcAdapter.enableReaderMode(this.getActivity(), 410 | this, 411 | NfcAdapter.FLAG_READER_NFC_A | 412 | NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS, 413 | options); 414 | } 415 | } 416 | 417 | @Override 418 | public void onPause() { 419 | super.onPause(); 420 | if (mNfcAdapter != null) 421 | mNfcAdapter.disableReaderMode(this.getActivity()); 422 | } 423 | 424 | } -------------------------------------------------------------------------------- /app/src/main/java/de/androidcrypto/android_hce_beginner_app/MyHostApduService.java: -------------------------------------------------------------------------------- 1 | package de.androidcrypto.android_hce_beginner_app;/* 2 | * Copyright (C) 2013 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import android.content.Context; 18 | import android.nfc.NdefMessage; 19 | import android.nfc.NdefRecord; 20 | import android.nfc.cardemulation.HostApduService; 21 | import android.os.Bundle; 22 | import android.os.Environment; 23 | import android.util.Log; 24 | import android.view.Gravity; 25 | import android.widget.Toast; 26 | 27 | import java.io.BufferedReader; 28 | import java.io.File; 29 | import java.io.FileReader; 30 | import java.io.IOException; 31 | import java.math.BigInteger; 32 | import java.util.Arrays; 33 | 34 | 35 | /** 36 | * This is a sample APDU Service which demonstrates how to interface with the card emulation support 37 | * added in Android 4.4, KitKat. 38 | * 39 | *

This sample replies to any requests sent with the string "Hello World". In real-world 40 | * situations, you would need to modify this code to implement your desired communication 41 | * protocol. 42 | * 43 | *

This sample will be invoked for any terminals selecting AIDs of 0xF11111111, 0xF22222222, or 44 | * 0xF33333333. See src/main/res/xml/aid_list.xml for more details. 45 | * 46 | *

Note: This is a low-level interface. Unlike the NdefMessage many developers 47 | * are familiar with for implementing Android Beam in apps, card emulation only provides a 48 | * byte-array based communication channel. It is left to developers to implement higher level 49 | * protocol support as needed. 50 | */ 51 | public class MyHostApduService extends HostApduService { 52 | private static final String TAG = "CardService"; 53 | // AID for our loyalty card service. 54 | private static final String SAMPLE_LOYALTY_CARD_AID = "F222222222"; 55 | // ISO-DEP command HEADER for selecting an AID. 56 | // Format: [Class | Instruction | Parameter 1 | Parameter 2] 57 | private static final String SELECT_APDU_HEADER = "00A40400"; 58 | // Format: [Class | Instruction | Parameter 1 | Parameter 2] 59 | private static final String GET_DATA_APDU_HEADER = "00CA0000"; 60 | // "OK" status word sent in response to SELECT AID command (0x9000) 61 | private static final byte[] SELECT_OK_SW = HexStringToByteArray("9000"); 62 | // "UNKNOWN" status word sent in response to invalid APDU command (0x0000) 63 | private static final byte[] UNKNOWN_CMD_SW = HexStringToByteArray("0000"); 64 | private static final byte[] SELECT_APDU = BuildSelectApdu(SAMPLE_LOYALTY_CARD_AID); 65 | private static final byte[] GET_DATA_APDU = BuildGetDataApdu(); 66 | 67 | // Commands for each step 68 | 69 | 70 | 71 | private static final byte[] SELECT_APDU_TagInfo = HexStringToByteArray("00A4040007D276000085010100"); 72 | private static final byte[] SELECT_APDU_TagInfo_OLD = { 73 | (byte)0x00, // CLA - Class - Class of instruction 74 | (byte)0xA4, // INS - Instruction - Instruction code 75 | (byte)0x04, // P1 - Parameter 1 - Instruction parameter 1 76 | (byte)0x00, // P2 - Parameter 2 - Instruction parameter 2 77 | (byte)0x07, // Lc field - Number of bytes present in the data field of the command 78 | //(byte)0xF0, (byte)0x39, (byte)0x41, (byte)0x48, (byte)0x14, (byte)0x81, (byte)0x00, // NDEF Tag Application name 79 | (byte)0xD2, (byte)0x76, (byte)0x00, (byte)0x00, (byte)0x85, (byte)0x01, (byte)0x01, // NDEF Tag Application name 80 | (byte)0x00 // Le field - Maximum number of bytes expected in the data field of the response to the command 81 | }; 82 | 83 | private static final byte[] GET_COMPATIBILITY_CONTAINER_APDU_OLD = HexStringToByteArray("00A4000C02E103"); 84 | private static final byte[] GET_CAPABILITY_CONTAINER_APDU = { 85 | (byte)0x00, // CLA - Class - Class of instruction 86 | (byte)0xa4, // INS - Instruction - Instruction code 87 | (byte)0x00, // P1 - Parameter 1 - Instruction parameter 1 88 | (byte)0x0c, // P2 - Parameter 2 - Instruction parameter 2 89 | (byte)0x02, // Lc field - Number of bytes present in the data field of the command 90 | (byte)0xe1, (byte)0x03 // file identifier of the CC file 91 | }; 92 | 93 | private static final byte[] READ_CAPABILITY_CONTAINER_APDU = { 94 | (byte)0x00, // CLA - Class - Class of instruction 95 | (byte)0xb0, // INS - Instruction - Instruction code 96 | (byte)0x00, // P1 - Parameter 1 - Instruction parameter 1 97 | (byte)0x00, // P2 - Parameter 2 - Instruction parameter 2 98 | (byte)0x0f // Lc field - Number of bytes present in the data field of the command 99 | }; 100 | 101 | // In the scenario that we have done a CC read, the same byte[] match 102 | // for ReadBinary would trigger and we don't want that in succession 103 | private boolean READ_CAPABILITY_CONTAINER_CHECK = false; 104 | 105 | private static final byte[] READ_CAPABILITY_CONTAINER_RESPONSE = { 106 | (byte)0x00, (byte)0x0F, // CCLEN length of the CC file 107 | (byte)0x20, // Mapping Version 2.0 108 | (byte)0xFF, (byte)0xFF, // MLe maximum 59 bytes R-APDU data size 109 | (byte)0xFF, (byte)0xFF, // MLc maximum 52 bytes C-APDU data size 110 | (byte)0x04, // T field of the NDEF File Control TLV 111 | (byte)0x06, // L field of the NDEF File Control TLV 112 | (byte)0xE1, (byte)0x04, // File Identifier of NDEF file 113 | (byte)0xFF, (byte)0xFE, // Maximum NDEF file size of 50 bytes 114 | (byte)0x00, // Read access without any security 115 | (byte)0xFF, // Write access without any security 116 | (byte)0x90, (byte)0x00 // A_OKAY 117 | }; 118 | 119 | private static final byte[] READ_CAPABILITY_CONTAINER_RESPONSE_ORG = { 120 | (byte)0x00, (byte)0x0F, // CCLEN length of the CC file 121 | (byte)0x20, // Mapping Version 2.0 122 | (byte)0x00, (byte)0x3B, // MLe maximum 59 bytes R-APDU data size 123 | (byte)0x00, (byte)0x34, // MLc maximum 52 bytes C-APDU data size 124 | (byte)0x04, // T field of the NDEF File Control TLV 125 | (byte)0x06, // L field of the NDEF File Control TLV 126 | (byte)0xE1, (byte)0x04, // File Identifier of NDEF file 127 | (byte)0x00, (byte)0x32, // Maximum NDEF file size of 50 bytes 128 | (byte)0x00, // Read access without any security 129 | (byte)0x00, // Write access without any security 130 | (byte)0x90, (byte)0x00 // A_OKAY 131 | }; 132 | 133 | private static final byte[] NDEF_SELECT_APDU = { 134 | (byte)0x00, // CLA - Class - Class of instruction 135 | (byte)0xa4, // Instruction byte (INS) for Select command 136 | (byte)0x00, // Parameter byte (P1), select by identifier 137 | (byte)0x0c, // Parameter byte (P1), select by identifier 138 | (byte)0x02, // Lc field - Number of bytes present in the data field of the command 139 | (byte)0xE1, (byte)0x04 // file identifier of the NDEF file retrieved from the CC file 140 | }; 141 | 142 | private static final byte[] NDEF_READ_BINARY_NLEN_APDU = { 143 | (byte)0x00, // Class byte (CLA) 144 | (byte)0xb0, // Instruction byte (INS) for ReadBinary command 145 | (byte)0x00, (byte)0x00, // Parameter byte (P1, P2), offset inside the CC file 146 | (byte)0x02 // Le field 147 | }; 148 | 149 | private static final byte[] NDEF_READ_BINARY_GET_NDEF_APDU = { 150 | (byte)0x00, // Class byte (CLA) 151 | (byte)0xb0, // Instruction byte (INS) for ReadBinary command 152 | (byte)0x00, (byte)0x02, // Parameter byte (P1, P2), offset inside the CC file 153 | (byte)0x13 // Le field 154 | }; 155 | 156 | private static final byte[] NDEF_READ_BINARY_GET_NDEF_APDU_ORG = { 157 | (byte)0x00, // Class byte (CLA) 158 | (byte)0xb0, // Instruction byte (INS) for ReadBinary command 159 | (byte)0x00, (byte)0x00, // Parameter byte (P1, P2), offset inside the CC file 160 | (byte)0x0f // Le field 161 | }; 162 | 163 | 164 | 165 | private byte[] NDEF_URI_BYTES = getNdefMessage("Hello world!"); 166 | private byte[] NDEF_URI_LEN = BigInteger.valueOf(NDEF_URI_BYTES.length).toByteArray(); 167 | 168 | private byte[] getNdefMessage(String ndefData) { 169 | if (ndefData.length() == 0) { 170 | return new byte[0]; 171 | } 172 | NdefMessage ndefMessage; 173 | NdefRecord ndefRecord; 174 | ndefRecord = NdefRecord.createTextRecord("en", ndefData); 175 | ndefMessage = new NdefMessage(ndefRecord); 176 | return ndefMessage.toByteArray(); 177 | } 178 | 179 | /*File IO Stuffs*/ 180 | File sdcard = Environment.getExternalStorageDirectory(); 181 | File file = new File(sdcard, "file.txt"); 182 | StringBuilder text = new StringBuilder(); 183 | int pointer; 184 | 185 | /** 186 | * Called if the connection to the NFC card is lost, in order to let the application know the 187 | * cause for the disconnection (either a lost link, or another AID being selected by the 188 | * reader). 189 | * 190 | * @param reason Either DEACTIVATION_LINK_LOSS or DEACTIVATION_DESELECTED 191 | */ 192 | @Override 193 | public void onDeactivated(int reason) { 194 | } 195 | 196 | /** 197 | * This method will be called when a command APDU has been received from a remote device. A 198 | * response APDU can be provided directly by returning a byte-array in this method. In general 199 | * response APDUs must be sent as quickly as possible, given the fact that the user is likely 200 | * holding his device over an NFC reader when this method is called. 201 | * 202 | *

If there are multiple services that have registered for the same AIDs in 203 | * their meta-data entry, you will only get called if the user has explicitly selected your 204 | * service, either as a default or just for the next tap. 205 | * 206 | *

This method is running on the main thread of your application. If you 207 | * cannot return a response APDU immediately, return null and use the {@link 208 | * #sendResponseApdu(byte[])} method later. 209 | * 210 | * @param commandApdu The APDU that received from the remote device 211 | * @param extras A bundle containing extra data. May be null. 212 | * @return a byte-array containing the response APDU, or null if no response APDU can be sent 213 | * at this point. 214 | */ 215 | 216 | @Override 217 | public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) { 218 | // The following flow is based on Appendix E "Example of Mapping Version 2.0 Command Flow" 219 | // in the NFC Forum specification 220 | Log.i(TAG, "Received APDU: " + ByteArrayToHexString(commandApdu)); 221 | // see https://github.com/underwindfall/NFCAndroid/blob/master/app/src/main/java/com/qifan/nfcbank/cardEmulation/KHostApduService.kt 222 | /* 223 | if (commandApdu.sliceArray(0..1).contentEquals(NDEF_READ_BINARY)) { 224 | // do nothing 225 | } 226 | */ 227 | 228 | // First command: NDEF Tag Application select (Section 5.5.2 in NFC Forum spec) 229 | if (Arrays.equals(SELECT_APDU_TagInfo, commandApdu)) { 230 | Log.i(TAG, "This is: 01 SELECT_APDU_TagInfo"); 231 | return SELECT_OK_SW; 232 | 233 | // Second command: Capability Container select (Section 5.5.3 in NFC Forum spec) 234 | } else if (Arrays.equals(GET_CAPABILITY_CONTAINER_APDU, commandApdu)) { 235 | Log.i(TAG, "This is: 02 GET_COMPATIBILITY_CONTAINER_APDU"); 236 | return SELECT_OK_SW; 237 | 238 | // Third command: ReadBinary data from CC file (Section 5.5.4 in NFC Forum spec) 239 | } else if ((Arrays.equals(READ_CAPABILITY_CONTAINER_APDU, commandApdu)) && (!READ_CAPABILITY_CONTAINER_CHECK)) { 240 | Log.i(TAG, "This is: 03 READ_COMPATIBILITY_CONTAINER_APDU"); 241 | Log.i(TAG, "Response: READ_CAPABILITY_CONTAINER_RESPONSE"); 242 | READ_CAPABILITY_CONTAINER_CHECK = true; 243 | return READ_CAPABILITY_CONTAINER_RESPONSE; 244 | 245 | // Fourth command: NDEF Select command (Section 5.5.5 in NFC Forum spec) 246 | } else if (Arrays.equals(NDEF_SELECT_APDU, commandApdu)) { 247 | Log.i(TAG, "This is: 04 NDEF_SELECT_APDU"); 248 | return SELECT_OK_SW; 249 | 250 | // Fifth command: ReadBinary, read NLEN field 251 | } else if (Arrays.equals(NDEF_READ_BINARY_NLEN_APDU, commandApdu)) { 252 | Log.i(TAG, "This is: 05 NDEF_READ_BINARY_NLEN_APDU"); 253 | /* 254 | byte[] start = { 255 | (byte)0x00 256 | }; 257 | // Build our response 258 | byte[] response = new byte[start.length + NDEF_URI_LEN.length + SELECT_OK_SW.length]; 259 | System.arraycopy(start, 0, response, 0, start.length); 260 | System.arraycopy(NDEF_URI_LEN, 0, response, start.length, NDEF_URI_LEN.length); 261 | System.arraycopy(SELECT_OK_SW, 0, response, start.length + NDEF_URI_LEN.length, SELECT_OK_SW.length); 262 | */ 263 | // new as in Underwindfall 264 | byte[] response = new byte[NDEF_URI_LEN.length + SELECT_OK_SW.length]; 265 | System.arraycopy(NDEF_URI_LEN, 0, response, 0, NDEF_URI_LEN.length); 266 | System.arraycopy(SELECT_OK_SW, 0, response, NDEF_URI_LEN.length, SELECT_OK_SW.length); 267 | Log.i(TAG, "NDEF_READ_BINARY_NLEN triggered. Our Response: " + ByteArrayToHexString(response)); 268 | return response; 269 | 270 | // Sixth command: ReadBinary, get NDEF data 271 | } else if (Arrays.equals(NDEF_READ_BINARY_GET_NDEF_APDU, commandApdu)) { 272 | Log.i(TAG, "This is: 06 NDEF_READ_BINARY_GET_NDEF_APDU"); 273 | byte[] start = { 274 | (byte)0x00 275 | }; 276 | // Build our response 277 | byte[] response = new byte[start.length + NDEF_URI_LEN.length + NDEF_URI_BYTES.length + SELECT_OK_SW.length]; 278 | System.arraycopy(start, 0, response, 0, start.length); 279 | System.arraycopy(NDEF_URI_LEN, 0, response, start.length, NDEF_URI_LEN.length); 280 | System.arraycopy(NDEF_URI_BYTES, 0, response, start.length + NDEF_URI_LEN.length, NDEF_URI_BYTES.length); 281 | System.arraycopy(SELECT_OK_SW, 0, response, start.length + NDEF_URI_LEN.length + NDEF_URI_BYTES.length, SELECT_OK_SW.length); 282 | //Log.i(TAG, NDEF_URI.toString()); 283 | Log.i(TAG, new String(NDEF_URI_BYTES)); 284 | Log.i(TAG, "NDEF_READ_BINARY_GET_NDEF triggered. Our Response: " + ByteArrayToHexString(response)); 285 | Log.i(TAG, "NDEF_READ_BINARY_GET_NDEF response length: " + response.length); 286 | Context context = getApplicationContext(); 287 | CharSequence text = "NDEF text has been sent to the reader, length is " + response.length; 288 | int duration = Toast.LENGTH_SHORT; 289 | Toast toast = Toast.makeText(context, text, duration); 290 | toast.setGravity(Gravity.CENTER, 0, 0); 291 | toast.show(); 292 | 293 | // print the ndef message 294 | text = ByteArrayToHexString(NDEF_URI_BYTES); 295 | toast = Toast.makeText(context, text, duration); 296 | toast.setGravity(Gravity.CENTER, 0, 0); 297 | toast.show(); 298 | READ_CAPABILITY_CONTAINER_CHECK = false; 299 | return response; 300 | // We're doing something outside our scope 301 | } else 302 | Log.wtf(TAG, "processCommandApdu() | I don't know what's going on!!!."); 303 | //return "Can I help you?".getBytes(); 304 | return UNKNOWN_CMD_SW; 305 | } 306 | 307 | 308 | 309 | /** 310 | * Build APDU for SELECT AID command. This command indicates which service a reader is 311 | * interested in communicating with. See ISO 7816-4. 312 | * 313 | * @param aid Application ID (AID) to select 314 | * @return APDU for SELECT AID command 315 | */ 316 | public static byte[] BuildSelectApdu (String aid){ 317 | // Format: [CLASS | INSTRUCTION | PARAMETER 1 | PARAMETER 2 | LENGTH | DATA] 318 | return HexStringToByteArray(SELECT_APDU_HEADER + String.format("%02X", 319 | aid.length() / 2) + aid); 320 | } 321 | 322 | /** 323 | * Build APDU for GET_DATA command. See ISO 7816-4. 324 | * 325 | * @return APDU for SELECT AID command 326 | */ 327 | public static byte[] BuildGetDataApdu () { 328 | // Format: [CLASS | INSTRUCTION | PARAMETER 1 | PARAMETER 2 | LENGTH | DATA] 329 | return HexStringToByteArray(GET_DATA_APDU_HEADER + "0FFF"); 330 | } 331 | 332 | /** 333 | * Utility method to convert a byte array to a hexadecimal string. 334 | * 335 | * @param bytes Bytes to convert 336 | * @return String, containing hexadecimal representation. 337 | */ 338 | public static String ByteArrayToHexString ( byte[] bytes){ 339 | final char[] hexArray = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; 340 | char[] hexChars = new char[bytes.length * 2]; // Each byte has two hex characters (nibbles) 341 | int v; 342 | for (int j = 0; j < bytes.length; j++) { 343 | v = bytes[j] & 0xFF; // Cast bytes[j] to int, treating as unsigned value 344 | hexChars[j * 2] = hexArray[v >>> 4]; // Select hex character from upper nibble 345 | hexChars[j * 2 + 1] = hexArray[v & 0x0F]; // Select hex character from lower nibble 346 | } 347 | return new String(hexChars); 348 | } 349 | 350 | /** 351 | * Utility method to convert a hexadecimal string to a byte string. 352 | * 353 | *

Behavior with input strings containing non-hexadecimal characters is undefined. 354 | * 355 | * @param s String containing hexadecimal characters to convert 356 | * @return Byte array generated from input 357 | * @throws java.lang.IllegalArgumentException if input length is incorrect 358 | */ 359 | public static byte[] HexStringToByteArray (String s) throws IllegalArgumentException { 360 | int len = s.length(); 361 | if (len % 2 == 1) { 362 | throw new IllegalArgumentException("Hex string must have even number of characters"); 363 | } 364 | byte[] data = new byte[len / 2]; // Allocate 1 byte per 2 hex characters 365 | for (int i = 0; i < len; i += 2) { 366 | // Convert each character into a integer (base-16), then bit-shift into place 367 | data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) 368 | + Character.digit(s.charAt(i + 1), 16)); 369 | } 370 | return data; 371 | } 372 | 373 | /** 374 | * Utility method to concatenate two byte arrays. 375 | * @param first First array 376 | * @param rest Any remaining arrays 377 | * @return Concatenated copy of input arrays 378 | */ 379 | public static byte[] ConcatArrays ( byte[] first, byte[]...rest){ 380 | int totalLength = first.length; 381 | for (byte[] array : rest) { 382 | totalLength += array.length; 383 | } 384 | byte[] result = Arrays.copyOf(first, totalLength); 385 | int offset = first.length; 386 | for (byte[] array : rest) { 387 | System.arraycopy(array, 0, result, offset, array.length); 388 | offset += array.length; 389 | } 390 | return result; 391 | } 392 | 393 | private void readFromFile () { 394 | try { 395 | BufferedReader br = new BufferedReader(new FileReader(file)); 396 | String line; 397 | 398 | while ((line = br.readLine()) != null) { 399 | text.append(line); 400 | text.append('\n'); 401 | } 402 | } catch (IOException e) { 403 | e.printStackTrace(); 404 | } 405 | } 406 | } 407 | --------------------------------------------------------------------------------