Tuesday, March 11, 2025

HCE: Host-based Card Emulation


As Sherlock Holmes might have said, had he lived 100 years later and been a software engineer, "once you have eliminated all the solutions that you can't get to work, whatever is left, however hacky, difficult and cumbersome, must be the right solution."

I think we have reached that point.

The one real avenue I have left is what the Android docs describe as host-based card emulation. Just the name sends me running away in fear. When I glanced through this section before, when I was just reviewing what the various technologies were, it seemed (particularly given its placement in the manual after "Advanced NFC") not something I was sure I would ever be willing to contemplate.

With my newfound knowledge and experience of failure, everything in this section suddenly clicks with me. They talk about APDUs (check), they talk about AIDs (check) and how they can be selected (check). And then it struck me: if, as a phone, I want to talk to an NFC reader, I want to be a card. Or at least emulate a card. It's only really the "host-based" that is confusing, and even that is explained fairly well in the article - it is card emulation done in software rather than a special purpose "secure" chip.

Not to say that it isn't hacky, difficult and cumbersome to go down this route. For starters, it requires us to implement an Android service, which, while by no means impossible, creates some significant issues for "toy" applications in communicating with a main activity that can interact with the user.

And we are going to need to build a lot of infrastructure before we can get anywhere.

But at least we know what we're trying to accomplish and how. Let's get started.

Application Outline

So here's the aiming point: the first example application I came up with was to imagine a PoS device that offered to give you your receipt on your phone by NFC. You tap your phone and the receipt is transferred and (temporarily) displayed. OK, sounds plausible, if hard.

Since it's going to take us quite a while to build that, what should we choose as out starting point? Well, based off the HCE documentation and the Go code we have for APDU, I want to build an application that can just say "yes" when it receives the appropriate SELECT AID action . I'll need to write a script on the Linux side for the apdu program that just sends that across and hopes for the best.

Building a Basic Application

So I'm back into Android Studio building another "Empty Views Activity" Application - the one that allows me to build in Java, rather than Kotlin.

I've checked the thing that is generated in as ANDROID_HCE_GENERATED.

Configuring the NFC HCE Service

The first thing I want to do is create the Service activity as described in the documentation:
package com.example.android_hce;

import android.app.Service;
import android.content.Intent;
import android.nfc.cardemulation.HostApduService;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;

public class ReceiveReceiptService extends HostApduService {
    public ReceiveReceiptService() {
    }

    @Override
    public byte[] processCommandApdu(byte[] apdu, Bundle extras) {
       Log.w("service", "processApdu");
       return new byte[] { (byte)0x90, 0x00 }; // APDU "OK"
    }
    @Override
    public void onDeactivated(int reason) {
        Log.w("service", "lost contact with card: " + reason);
    }
}
This is about as minimal as I can reasonably make it. The APDU handler is expecting a response, so I'm passing back 90 00 which I believe is the correct way to phrase "OK". It's possible that I need to also pass back a leading 00 to say I'm not sending any data back.

And then I need to configure this service in the AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-feature android:name="android.hardware.nfc.hce" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AndroidHCE"
        tools:targetApi="31">
        <service
            android:name=".ReceiveReceiptService"
            android:enabled="true"
            android:exported="true"
            android:permission="android.permission.BIND_NFC_SERVICE"
        >
            <intent-filter>
                <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/>
            </intent-filter>
            <meta-data
                android:name="android.nfc.cardemulation.host_apdu_service"
                android:resource="@xml/apduservice"/>
        </service>

        <activity
            android:name=".MainActivity"
            android:exported="true">
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MAIN" />-->
<!-- <category android:name="android.intent.category.LAUNCHER" />-->
<!-- </intent-filter>-->
        </activity>
    </application>

</manifest>

ANDROID_HCE_SERVICE:android_hce/app/src/main/AndroidManifest.xml

Note the fact that we are "using" the "feature" nfc.hce.

Because (for now at least) we only want this app to respond to contact with a suitable terminal, I have disabled the default LAUNCHER intent filter associated with the main activity. I should probably rename that (and may do later) but for now, I'm just trying to get something working. Other than that, all I have basically done here is to copy and paste the example code from the documentation into the generated manifest with some name changes.

Which in turn requires me to create an XML file apduservice.xml specifying an ID.

Choosing an AID

Most of the file apduservice.xml is again copied from the documentation. But it does require you to select a "unique" application id (AID) and you are supposed to register these to avoid conflict. But it occurs to me that generally these days we avoid conflict through the use of DNS (as with Java package names and using email addresses as unique identifiers), so I will just take the bytes of my base DNS name and add an "app1" on the end, hence gmmapowell-app1.

The value needs to be in hex, though, so I can convert that on the command line:
$ echo -n "gmmapowell-app1" | od -t x1
0000000 67 6d 6d 61 70 6f 77 65 6c 6c 2d 61 70 70 31
Sadly, it's not quite that simple and there are rules: specifically, according to the ISO spec, the first nibble of the aid needs to be 'F' for unregistered applications. So, given that I have one byte left (one of the rules it has to be no more than 16 bytes), I'll just tack an F1 (who doesn't like grand prix motor cars?) on the front.

And thus complete the $apduservice.xml file:
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/servicedesc"
    android:requireDeviceUnlock="false">
    <aid-group android:description="@string/aiddescription" android:category="other">
        <aid-filter android:name="F1676D6D61706F77656C6C2D61707031"/>
    </aid-group>
</host-apdu-service>

Building from Android Studio

I am slowly starting to get to grips with "the new" Android Studio (the last one I used, several years ago, was based on Eclipse, where the new one is based on IDEA and defaults to dark mode). I think I've managed to turn the annoying spellcheck feature off (File>Settings>Inspections) although I think I may have turned a lot more things off besides. And I have figured out how to build and run directly from Android Studio: there's a little picture of a bug towards the top of the screen. Click that, and magical things happen.

The application now builds and loads onto the device. Because I disabled the LAUNCHER intent filter, nothing much appears to happen, but hopefully it is running in the background (yes, I know I could check).

Sending a Notification

Now I've got to the point where I believe that the application is corectly configured and should be invoked when the appropriate APDU commands are invoked. But there are two ways I can go from here: try it and see if I can tell that it's working; or add code to alert the user that it has been contacted. I think the latter is sufficiently easy that I'll give it a go. Admittedly, I've added logging statements to the service, but I really don't have any great faith that they will come out after the last app produced nothing even when it was clearly running.

I was thinking that I could simply send out an intent from my service and that would launch MainActivity. Sadly, this is no longer the case. Since Android 10 (OK, it's a long while since I last did Android development) it has apparently been the case that instead you need to show a notification and get the user to click on the notification before an activity is launched.

In itself, this doesn't worry me unduly. Except, looking at it, even putting a notification up seems a lot harder these days. Oh, well, let's get started.

I'm working from this Android documentation which appears to describe exactly what I want to do, except it wants to do it in Kotlin :-( I'm starting to feel like a dinosaur. Except I don't see the point of learning Kotlin: if I have to learn everything all over again, why not JavaScript, Go or Rust?

Preparing to Send Notifications

So it seems the first thing we need to do is to configure our application to say that we want to send notifications. Even this is a multi-step process.

First up, we need to register that this is a permission we want in the manifest:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-feature android:name="android.hardware.nfc.hce" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

Then we need to give the application a name so that we can implement an Application subclass that has some setup code in it.
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:name=".ReceiptApplication"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AndroidHCE"
        tools:targetApi="31">

ANDROID_HCE_NOTIFICATIONS:android_hce/app/src/main/AndroidManifest.xml

This enables us to implement the Application subclass:
package com.example.android_hce;

import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;

public class ReceiptApplication extends Application {
    public static String CHANNEL_ID = "gmmapowell.com:receipts/notifications";

    @Override
    public void onCreate() {
        super.onCreate();
        NotificationChannel channel = new NotificationChannel(
                CHANNEL_ID,
                "Notifications for Receipts",
                NotificationManager.IMPORTANCE_HIGH
        );

        NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.createNotificationChannel(channel);
    }
}

ANDROID_HCE_NOTIFICATIONS:android_hce/app/src/main/java/com/example/android_hce/ReceiptApplication.java

Since I basically just copied this out of the Android documentation, I can't honestly say I understand what it does, except that it is preparing the way for a "notification channel" - some way in which our service can pin a notification up on the title bar. I do understand that the CHANNEL_ID is expected to be unique so that when we use the same name later, it finds the right notification channel.

But this is not enough. We need to have the user explicitly give us the permission to send notifications, so come back MainActivity; all is forgiven. We need to add a button to this which the user can click to set off the process for asking for permissions.

We need to uncomment the intent-filter from AndroidManifest.xml:
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

ANDROID_HCE_NOTIFICATIONS:android_hce/app/src/main/AndroidManifest.xml

And then add so much code to it that I might as well just show the whole thing as if it's from scratch:
package com.example.android_hce;

import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.Manifest;

import androidx.activity.EdgeToEdge;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity {
    boolean hasNotificationPermission;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        Button rp = findViewById(R.id.request_permission);
        rp.setOnClickListener(new HandlePermissions());
    }

    // Register the permissions callback, which handles the user's response to the
    // system permissions dialog. Save the return value, an instance of
    // ActivityResultLauncher, as an instance variable.
    private ActivityResultLauncher<String> requestPermissionLauncher =
        registerForActivityResult(new RequestPermission(), isGranted -> {
            if (isGranted) {
                // Permission is granted. Continue the action or workflow in your
                // app.
                Log.w("perms", "Permission Granted");
                hasNotificationPermission = true;
            } else {
                // Explain to the user that the feature is unavailable because the
                // feature requires a permission that the user has denied. At the
                // same time, respect the user's decision. Don't link to system
                // settings in an effort to convince the user to change their
                // decision.
                Log.w("perms", "Permission Denied");
            }
        });

    private class HandlePermissions implements View.OnClickListener {
        @Override
        public void onClick(View view) {
            if (!hasNotificationPermission) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
                }
            }
        }
    }
}

ANDROID_HCE_NOTIFICATIONS:android_hce/app/src/main/java/com/example/android_hce/MainActivity.java

Again, I hesitate to describe code I have just basically cut and pasted from the documentation (with a bit of converting Kotlin to Java), But, in brief, in the (existing) onCreate method, we add a call to add a click handler to a button we have added to the layout. This is implemented by the HandlePermissions nested class, which first checks if we have the permission (the use of a class-level variable seems dubious to me, but I copy what I see, and given that the whole code wasn't shown, there could be an implication of setting this in onCreate) and then if we need to ask for the permission, invokes a "permission launcher" which appears to be some kind of helper that does all the workflow of requesting permissions.

And I'll leave it at that.

So now we can run the application, push the button and enable the permissions.

Sending the Notification

Now we need to wire up the service to create and deliver a notification. This obviously needs to happen in the code that processes the command APDU:
    public byte[] processCommandApdu(byte[] apdu, Bundle extras) {
       Log.w("service", "processApdu");
       showNotification();
       return new byte[] { (byte)0x90, 0x00 }; // APDU "OK"
    }

ANDROID_HCE_NOTIFY_AID:android_hce/app/src/main/java/com/example/android_hce/ReceiveReceiptService.java

And we can easily implement this method by copying and translating the example from the Android Documentation:
    private void showNotification() {
        int notificationId = 99; // is this more a constant or more a per request ID?

        NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        NotificationCompat.Builder notificationBuilder =
                new NotificationCompat.Builder(this, ReceiptApplication.CHANNEL_ID)
                        .setSmallIcon(R.drawable.credit_card)
                        .setContentTitle("NFC")
                        .setContentText("NFC Contact Made")
                        .setPriority(NotificationCompat.PRIORITY_HIGH)
                        .setCategory(NotificationCompat.CATEGORY_STATUS);

        notificationManager.notify(notificationId, notificationBuilder.build());
    }

ANDROID_HCE_NOTIFY_AID:android_hce/app/src/main/java/com/example/android_hce/ReceiveReceiptService.java

So now, when we receive a request from the NFC reader, it should automatically invoke this code which should pop up a notification.

As this starts up, I see some message about permissions and NFC but I couldn't capture it to show here, but I am going to add the relevant permission into the AndroidManifest, as I suspect the message was to tell me nothing would work without it.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-feature android:name="android.hardware.nfc.hce" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    <uses-permission android:name="android.permission.NFC"/>

ANDROID_HCE_NOTIFY_AID:android_hce/app/src/main/AndroidManifest.xml

Invoking this from the Reader

We should be able to just do a "SELECT <AID>" from the apdu command and everything should work. Let's create a script with that in, which I will call launchReceipt.apdu
00 A4 04 00 10 F1 67 6d 6d 61 70 6f 77 65 6c 6c 2d 61 70 70 31 00
I would like to put this on separate lines so that it is clear what is going on, but the current Go code I have stolen does not support that, and by the time I've finished fixing it so that it does, I will be writing a complete application that won't be reading input from a script anyway. But for clarity, this amounts to:
# The bytes for the APDU "SELECT AID" command
00 A4 04 00


# The length of my AID: 16 bytes (10 hex)
10


#F1gmmapowell-app1
F1 67 6d 6d 61 70 6f 77 65 6c 6c 2d 61 70 70 31 # My AID,


# We don't expect a response
00
Now let's run APDU.
>> 00 A4 04 00 10 F1676D6D61706F77656C6C2D61707031 00
<< 9000
And, wow. Just like that it all works and the notification is up on my screen, credit card icon and all.

I may be speaking too soon, but I feel that "the hard bit" is over, and from here it's just a simple matter of programming(TM).

Conclusion

Having been initially deterred from even looking into "HCE mode" because it seemed to scary and gnarly, it turns out to be just what I need and not too hard.

With this as a foundation, we can start to build both sides of our receipt app. I really feel that all of the "hard work" of discovery is over and we can start writing code in earnest.

No comments:

Post a Comment