Sharing the Thread Network With Google Thread Credentials APIs

1. Before you begin

In our Thread Border Router (TBR) codelab, we show how to build a Thread Border Router based on a Raspberry Pi. In that codelab we

  • Establish Bidirectional IP connectivity between Thread and Wi-Fi/Ethernet networks.
  • Provide Bidirectional service discovery via mDNS (on Wi-Fi/Ethernet link) and SRP (on Thread network).

This codelab builds on the previous one, addressing how your own border router and your app can interact with Google APIs to make a single Thread Network. Converging Thread credentials is important as it adds to the network robustness and simplifies user interactions with the applications that rely on Thread.

Prerequisites

  • Complete the OTBR Codelab
  • Basic knowledge of Linux, Android/Kotlin, and Thread networking

What you'll learn

  • How to use the Thread Sharing APIs to Get and Set credential sets
  • How to set up your own OpenThread Border Router with the same credentials as Google's network

What you'll need

  • Raspberry Pi 4 board or another Linux-based board running the Open Thread Border Router (OTBR)
  • Board that provides IEEE 802.15.4 connectivity as a Radio Co-Processor (RCP). See a list of repositories of different SoC vendors and their instructions on the OpenThread Github page

2. Setting up HTTP service

The first building block we need is an interface that allows us to read Active Credentials and write Pending Credentials to your OTBR. When building a TBR, use your own proprietary mechanisms, as shown here with two examples. The first option showcases how to interface with the OTBR agent locally via DBUS, while the second leverages the Rest API that can be built on the OTBR.

Neither method is secure, and should not be used as-is in a production environment. However, a vendor can build encryption around either method to use it in a production environment, or you can extend your own monitor service to issue loopback HTTP or inherently local DBUS calls.

Option 1: DBUS and HTTP API on Python Script

91e5fdeed83e9354.png

This step creates a bare-bones HTTP service that exposes two endpoints to read and set credentials, ultimately calling DBUS commands.

On the RPi that will serve as your OTBR, install the Python 3 dependencies:

$ pip install dbus-python shlex json

Run the script as:

$  sudo python credentials_server.py 8081
serving at port 8081

The sample sets up an HTTP server on port 8081 and listens on the root path either for a GET request to retrieve Thread credentials, or a POST request to set Thread credentials. The payload is always a JSON structure with the TLV.

The following PUT request sets new Pending Thread Credentials to the OTBR via the /node/dataset/pending path. In this case, the pending credentials are applied in 10 seconds:

PUT /node/dataset/pending
Host: <IP>:8081
ContentType: "application/json"
acceptMimeType: "application/json"
...
{
        "ActiveDataset": "<TLV encoded new Thread Dataset>"
"PendingTimestamp": {
        "Seconds": <Unix timestamp in seconds>,
        "Ticks": 0,
        "Authoritative": false
},
"Delay": 10000 // in milliseconds
}

A GET request to /node/dataset/active fetches the currently Active Credentials.

GET /node/dataset/active
Host: <IP>:8081
ContentType = "application/json"
acceptMimeType = "text/plain"
...
<TLV encoded Thread Dataset>

The script calls DBUS R/W commands to the bus path io.openthread.BorderRouter.wpan0, object path /io/openthread/BorderRouter/wpan0

# D-BUS interface
def call_dbus_method(interface, method_name, *arguments):
    bus = dbus.SystemBus()
    obj = bus.get_object('io.openthread.BorderRouter.wpan0', '/io/openthread/BorderRouter/wpan0')
    iface = dbus.Interface(obj, interface)
    method = getattr(iface, method_name)
    res = method(*arguments)
    return res

def get_dbus_property(property_name):
    return call_dbus_method('org.freedesktop.DBus.Properties', 'Get', 'io.openthread.BorderRouter',
                                 property_name)

def set_dbus_property(property_name, property_value):
    return call_dbus_method('org.freedesktop.DBus.Properties', 'Set', 'io.openthread.BorderRouter',
                                 property_name, property_value)                               

DBUS allows introspection of its capabilities. You may do this as:

$ sudo dbus-send --system --dest=io.openthread.BorderRouter.wpan0 \
        --type=method_call --print-reply /io/openthread/BorderRouter/wpan0 \
        org.freedesktop.DBus.Introspectable.Introspect

You can also check the supported capabilities documented here.

Option 2: OTBR Agent native HTTP Rest API

c748ca5151b6cacb.png

The OpenThread Border Router builds by default with the flag REST_API=1, enabling the REST API. In case your build from a previous codelab did not enable the REST API, make sure to build OTBR on your RPi with that flag:

$ REST_API=1 INFRA_IF_NAME=wlan0 ./script/setup

An OTBR agent can be restarted by running:

$ sudo systemctl restart otbr-agent.service

The agent starts an HTTP server on port 8081. This server allows a user or monitor program to perform many tasks in the OTBR (documented here). You can use your browser, curl or wget to inspect its contents. Among the many supported paths are the use cases described above, with the GET verb on /node/dataset/active and the PUT verb on /node/dataset/pending

3. Setting up Credential Framework on Android

Preferred Credentials

Google Play Services on Android allows and expects the registration of credentials for all the TBRs in your network. Each is identified by its Border Router Agent ID (BAID). You'll use the addCredentials() method of the ThreadNetworkClient interface to perform this task. The first TBR that is added to the Google Play Services storage determines the Preferred Credentials for this mobile device.

The app that adds a set of Thread network credentials to its BAID becomes the owner of the credentials, and has full permissions to access them. If you try to access credentials added by other apps, you'll receive a PERMISSION_DENIED error. However, the preferred credentials are always available for any app upon user consent. We recommend that you keep credentials that are stored in Google Play services up-to-date when the Thread Border Router network is updated. While that information is not used today, we may provide enhanced journeys in the future.

Even if the first TBR is later excluded, the Preferred Credentials will persist on the Android device. Once set, other Apps that manage Thread credentials may obtain the credentials from a getPreferredCredentials() call.

Google TBR Sync

Android devices sync with Google TBRs automatically. If no credentials exist on Android, the devices extract them from Google TBRs in your network, and those credentials become the Preferred Credentials. Syncing between TBRs and the Android device happens only if the TBR is paired with a single user, or if it's paired with two users that are in the same Smart Home ( Structure).

This process will also happen when another Google user is on GHA for Android or GHA for iOS when the user is in the same Structure. In the case of GHA for iOS, the preferred credentials are set on iOS storage, if no preferred credentials exist.

If two Android devices (or Android + iGHA) exist in the same network with different sets of preferred credentials, the device that originally configured the TBR will prevail on the TBR.

3rd-party TBR onboarding

The credential's storage is currently not scoped by the user's Smart Home ( Structure). Each Android device will have its BAID storage, but once there is a Google TBR in the network, other Android devices and iOS devices running Google Home App for iOS will sync with that TBR and try to set local credentials on phone storage.

Before a new OOB TBR creates a network, it is important to check whether a preferred network already exists in Android's storage.

  • If a preferred network exists, the vendor should use it. This ensures that Thread devices are connected to a single Thread network when possible.
  • When no preferred network exists, create a new credential set and assign it to your TBR in Google Play Services. Android will honor those credentials as the standard credentials set on all Google-based TBRs, and other vendors will be able to enhance your mesh reach and robustness with additional devices

cd8bc726f67b1fa1.png

4. Cloning and Modifying your Android App

We've created an Android App that showcases the main possible calls to the Thread API. You can use these patterns in your app. In this codelab we'll clone the Google Home Sample App for Matter from Github.

All the source code shown here is already coded in the sample app. You're invited to modify it to your own needs, but you can simply clone the app or run the pre-built binaries to inspected the functionality.

  1. Clone it using:
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
  1. Download and open Android Studio.
  2. Click on File > Open and point to your cloned repository.
  3. Enable the developer mode on your Android phone.
  4. Connect it to your computer via an USB cable.
  5. Run the App from Android Studio via <Cmd+R> (OS X) or <Ctrl+R> (Win, Linux)
  6. Navigate to the Wheel -> Developer Utilities -> Thread Network
  7. Interact with the different options available. In the sections below we'll unpack the code that is executed on every button.

Do preferred credentials exist?

The first question a TBR manufacturer should ask Google is whether a preferred set of credentials already exists in the device. This should be the starting point of your flow. The code below queries GPS on the existence of credentials. It does not prompt for user consent because no credential is shared.

/**
* Prompts whether credentials exist in storage or not. Consent from user is not necessary
*/

fun doGPSPreferredCredsExist(activity: FragmentActivity) {
 try {
   // Uses the ThreadNetwork interface for the preferred credentials, adding
   // a listener that will receive an intentSenderResult. If that is NULL, 
   // preferred credentials don't exist. If that isn't NULL, they exist.
   // In this case we'll not use it.

   ThreadNetwork.getClient(activity).preferredCredentials.addOnSuccessListener { intentSenderResult ->
     intentSenderResult.intentSender?.let { intentSender ->
       ToastTimber.d("threadClient: preferred credentials exist", activity)
       // don't post the intent on `threadClientIntentSender` as we do when
       // we really want to know which are the credentials. That will prompt a
       // user consent. In this case we just want to know whether they exist
     } ?: ToastTimber.d(
       "threadClient: no preferred credentials found, or no thread module found", activity
     )
   }.addOnFailureListener { e: Exception ->
     Timber.d("ERROR: [${e}]")
   }
 } catch (e: Exception) {
   ToastTimber.e("Error $e", activity)
 }
}

Getting GPS preferred credentials

In case they exist, you want to read the credentials. The only difference from the previous code is that after receiving the intentSenderResult, you want to build and launch an intent using that result from the sender.

In our code, for organization/architecture purposes we use a MutableLiveData<IntentSender?> because the original code is in the ViewModel (ThreadViewModel.kt) and the intent observers are in the Activity Fragment ( ThreadFragment.kt). Thus, once the intentSenderResult is posted to the live data, we will execute the contents of this observer:

viewModel.threadClientIntentSender.observe(viewLifecycleOwner) { sender ->
 Timber.d(
   "threadClient: intent observe is called with [${intentSenderToString(sender)}]"
 )
 if (sender != null) {
   Timber.d("threadClient: Launch GPS activity to get ThreadClient")
   threadClientLauncher.launch(IntentSenderRequest.Builder(sender).build())
   viewModel.consumeThreadClientIntentSender()
 }
}

This will trigger user consent with sharing credentials, and, if approved, will return contents via:

threadClientLauncher =
 registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
   if (result.resultCode == RESULT_OK) {
     val threadNetworkCredentials =
       ThreadNetworkCredentials.fromIntentSenderResultData(result.data!!)
     viewModel.threadPreferredCredentialsOperationalDataset.postValue(
       threadNetworkCredentials
     )
   } else {
     val error = "User denied request."
     Timber.d(error)
     updateThreadInfo(null, "")
   }
 }

Posting the credentials to MutableLiveData<ThreadNetworkCredentials?> is described below.

Setting GPS credentials

Whether they exist or not, you should register your TBR into Google Play Services. Your app will be the only one able to read the credentials associated with the Border Agent ID of your TBR, but if your TBR is the first to register, those credentials will be copied to the Preferred Credentials set. That information is accessible to any App on the phone, as long as the user authorizes it.

/**
* Last step in setting the GPS thread credentials of a TBR
*/
private fun associateGPSThreadCredentialsToThreadBorderRouterAgent(
 credentials: ThreadNetworkCredentials?,
 activity: FragmentActivity,
 threadBorderAgent: ThreadBorderAgent,
) {
 credentials?.let {
   ThreadNetwork.getClient(activity).addCredentials(threadBorderAgent, credentials)
     .addOnSuccessListener {
       ToastTimber.d("threadClient: Credentials added", activity)
     }.addOnFailureListener { e: Exception ->
       ToastTimber.e("threadClient: Error adding the new credentials: $e", activity)
     }
 }
}

Setting the credentials to your TBR Product

This part is proprietary to each vendor, and in this codelab we implement it via either DBUS+Python HTTP Rest Server or the native HTTP Rest Server from OTBR.

/**
* Creates credentials in the format used by the OTBR HTTP server. See its documentation in
* https://github.com/openthread/ot-br-posix/blob/main/src/rest/openapi.yaml#L215
*/
fun createJsonCredentialsObject(newCredentials: ThreadNetworkCredentials): JSONObject {
 val jsonTimestamp = JSONObject()
 jsonTimestamp.put("Seconds", System.currentTimeMillis() / 1000)
 jsonTimestamp.put("Ticks", 0)
 jsonTimestamp.put("Authoritative", false)

 val jsonQuery = JSONObject()
 jsonQuery.put(
   "ActiveDataset",
   BaseEncoding.base16().encode(newCredentials.activeOperationalDataset)
 )
 jsonQuery.put("PendingTimestamp", jsonTimestamp)
 // delay of committing the pending set into active set: 10000ms
 jsonQuery.put("Delay", 10000)

 Timber.d(jsonQuery.toString())

 return jsonQuery
}

//(...)

var response = OtbrHttpClient.createJsonHttpRequest(
 URL("http://$ipAddress:$otbrPort$otbrDatasetPendingEndpoint"),
 activity,
 OtbrHttpClient.Verbs.PUT,
 jsonQuery.toString()
)

Getting the credentials from your TBR Product

As shown earlier, use the GET HTTP Verb for obtaining the credentials from your TBR. See the sample Python script.

Build and Imports

When creating your Android App, you'll need to make changes to your manifest, build, and imports to support the Google Play Services Thread Module. The following three snippets summarize most of the additions.

Note that our sample app primarily is built for Matter commissioning. Therefore, its Manifest and Gradle files are more complex than the additions necessary for just using Thread Credentials.

Manifest changes

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    (...)
    <!-- usesCleartextTraffic needed for OTBR local unencrypted communication -->
    <!-- Not needed for Thread Module, only used for HTTP -->
    <uses-feature
    (...)
        android:usesCleartextTraffic="true">

    <application>
    (...)
    <!-- GPS automatically downloads scanner module when app is installed -->
    <!-- Not needed for Thread Module, only used for scanning QR Codes -->
    <meta-data
        android:name="com.google.mlkit.vision.DEPENDENCIES"
        android:value="barcode_ui"/>
    </application>
</manifest>

Build.gradle

// Thread Network
implementation 'com.google.android.gms:play-services-threadnetwork:16.0.0'
// Thread QR Code Scanning
implementation 'com.google.android.gms:play-services-code-scanner:16.0.0'
// Thread QR Code Generation
implementation 'com.journeyapps:zxing-android-embedded:4.1.0'
// Needed for using BaseEncoding class
implementation 'com.google.guava:guava:31.1-jre'

Relevant Imports

// Thread Network Module
import com.google.android.gms.threadnetwork.ThreadNetworkCredentials
import com.google.android.gms.threadnetwork.ThreadBorderAgent
import com.google.android.gms.threadnetwork.ThreadNetwork

// Conversion of credentials to/fro Base16 (hex)
import com.google.common.io.BaseEncoding

// HTTP
import java.io.BufferedInputStream
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets

// Co-routines for HTTP calls
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch


// JSON
import org.json.JSONObject

// Logs
import timber.log.Timber

// mDNS/SD
import android.net.nsd.NsdServiceInfo

// QR Code reader / writer
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.journeyapps.barcodescanner.BarcodeEncoder

5. mDNS/SD Discovery

Our sample app uses mDNS/SD discovery to create a list of available Thread Border Routers in the network, as well as their respective BAIDs.

This is very helpful when entering the information of your TBR into GPS credential's storage. However, its usage is beyond the scope of this codelab. We use Android Service Discovery library NSDManager, and the full source code is available in the Sample App, in ServiceDiscovery.kt.

6. Putting everything together

Once you've implemented these calls or use the Sample App, you can fully onboard your RPi OTBR. Our Sample App exposes 8 buttons:

91979bf065e9673d.png

A possible sequence for onboarding your TBR is:

  1. Query whether preferential credentials exist (blue, 1st row)
  2. Depending on the answer
  3. Get GPS preferred credentials (blue, 2nd row)
  4. Set TBR credentials in GPS (blue, 3rd row) -> Select your TBR -> Create Random -> Enter network name -> Ok
  5. Now that you have preferred credentials, set them to your OTBR using Set RPi OTBR credentials, which will apply those credentials to the pending set.

The default for the sample app is to use 10 seconds delay. Thus after this period, the credentials of your RPi TBR (and other nodes that might exist on its network) will migrate to the new dataset.

7. Conclusion

In this codelab, we cloned a sample Android App and analyzed several snippets of code that make use of the Google Play Services' Thread Storage APIs. We used those APIs to have a common dataset we can onboard on a RPi TBR, which showcases a vendor's TBR.

Having all of a user's TBR in the same network improves the resilience and reach of the Thread Network. It also prevents flawed user journeys where apps cannot onboard Thread Devices because they don't have access to credentials.

We hope this codelab and Sample Apps help you design and develop your own App and your Thread Border Router product.

8. References

RCP co-processor

DBUS