diff --git a/common/thread_flags.aconfig b/common/thread_flags.aconfig index c11c6c0d45..14b70d0563 100644 --- a/common/thread_flags.aconfig +++ b/common/thread_flags.aconfig @@ -26,3 +26,12 @@ flag { description: "Controls whether the Android Thread setting max power of channel feature is enabled" bug: "346686506" } + +flag { + name: "epskc_enabled" + is_exported: true + is_fixed_read_only: true + namespace: "thread_network" + description: "Controls whether the Android Thread Ephemeral Key feature is enabled" + bug: "348323500" +} diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt index 9f26bcff76..09a3681f4e 100644 --- a/framework-t/api/system-current.txt +++ b/framework-t/api/system-current.txt @@ -507,7 +507,10 @@ package android.net.thread { } @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkController { + method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void activateEphemeralKeyMode(@NonNull java.time.Duration, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver); method public void createRandomizedDataset(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver); + method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void deactivateEphemeralKeyMode(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver); + method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @NonNull public java.time.Duration getMaxEphemeralKeyLifetime(); method public int getThreadVersion(); method public static boolean isAttached(int); method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void join(@NonNull android.net.thread.ActiveOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver); @@ -526,6 +529,9 @@ package android.net.thread { field public static final int DEVICE_ROLE_LEADER = 4; // 0x4 field public static final int DEVICE_ROLE_ROUTER = 3; // 0x3 field public static final int DEVICE_ROLE_STOPPED = 0; // 0x0 + field @FlaggedApi("com.android.net.thread.flags.epskc_enabled") public static final int EPHEMERAL_KEY_DISABLED = 0; // 0x0 + field @FlaggedApi("com.android.net.thread.flags.epskc_enabled") public static final int EPHEMERAL_KEY_ENABLED = 1; // 0x1 + field @FlaggedApi("com.android.net.thread.flags.epskc_enabled") public static final int EPHEMERAL_KEY_IN_USE = 2; // 0x2 field public static final int MAX_POWER_CHANNEL_DISABLED = -2147483648; // 0x80000000 field public static final int STATE_DISABLED = 0; // 0x0 field public static final int STATE_DISABLING = 2; // 0x2 @@ -540,6 +546,7 @@ package android.net.thread { public static interface ThreadNetworkController.StateCallback { method public void onDeviceRoleChanged(int); + method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public default void onEphemeralKeyStateChanged(int, @Nullable String, @Nullable java.time.Instant); method public default void onPartitionIdChanged(long); method public default void onThreadEnableStateChanged(int); } diff --git a/thread/framework/java/android/net/thread/IStateCallback.aidl b/thread/framework/java/android/net/thread/IStateCallback.aidl index 9d0a57151b..57c365b554 100644 --- a/thread/framework/java/android/net/thread/IStateCallback.aidl +++ b/thread/framework/java/android/net/thread/IStateCallback.aidl @@ -23,4 +23,6 @@ oneway interface IStateCallback { void onDeviceRoleChanged(int deviceRole); void onPartitionIdChanged(long partitionId); void onThreadEnableStateChanged(int enabledState); + void onEphemeralKeyStateChanged( + int ephemeralKeyState, @nullable String ephemeralKey, long expiryMillis); } diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl index b7f68c9950..e9cbb83e0c 100644 --- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl +++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl @@ -53,4 +53,7 @@ interface IThreadNetworkController { void setConfiguration(in ThreadConfiguration config, in IOperationReceiver receiver); void registerConfigurationCallback(in IConfigurationReceiver receiver); void unregisterConfigurationCallback(in IConfigurationReceiver receiver); + + void activateEphemeralKeyMode(long lifetimeMillis, in IOperationReceiver receiver); + void deactivateEphemeralKeyMode(in IOperationReceiver receiver); } diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java index cb4e8def82..1222398f67 100644 --- a/thread/framework/java/android/net/thread/ThreadNetworkController.java +++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java @@ -40,6 +40,7 @@ import com.android.net.thread.flags.Flags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.time.Duration; +import java.time.Instant; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; @@ -82,6 +83,25 @@ public final class ThreadNetworkController { /** The Thread radio is being disabled. */ public static final int STATE_DISABLING = 2; + /** The ephemeral key mode is disabled. */ + @FlaggedApi(Flags.FLAG_EPSKC_ENABLED) + public static final int EPHEMERAL_KEY_DISABLED = 0; + + /** + * The ephemeral key mode is enabled, an external commissioner candidate can use the ephemeral + * key to connect to this device and get Thread credential shared. + */ + @FlaggedApi(Flags.FLAG_EPSKC_ENABLED) + public static final int EPHEMERAL_KEY_ENABLED = 1; + + /** + * The ephemeral key is in use. This state means there is already an active secure session + * connected to this device with the ephemeral key, it's not possible to use the ephemeral key + * for new connections in this state. + */ + @FlaggedApi(Flags.FLAG_EPSKC_ENABLED) + public static final int EPHEMERAL_KEY_IN_USE = 2; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -100,6 +120,13 @@ public final class ThreadNetworkController { value = {STATE_DISABLED, STATE_ENABLED, STATE_DISABLING}) public @interface EnabledState {} + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef( + prefix = {"EPHEMERAL_KEY_"}, + value = {EPHEMERAL_KEY_DISABLED, EPHEMERAL_KEY_ENABLED, EPHEMERAL_KEY_IN_USE}) + public @interface EphemeralKeyState {} + /** Thread standard version 1.3. */ public static final int THREAD_VERSION_1_3 = 4; @@ -110,6 +137,9 @@ public final class ThreadNetworkController { @SuppressLint("MinMaxConstant") public static final int MAX_POWER_CHANNEL_DISABLED = Integer.MIN_VALUE; + /** The maximum lifetime of an ephemeral key. @hide */ + @NonNull private static final Duration EPHEMERAL_KEY_LIFETIME_MAX = Duration.ofMinutes(10); + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef({THREAD_VERSION_1_3}) @@ -174,6 +204,87 @@ public final class ThreadNetworkController { } } + /** Returns the maximum lifetime allowed when activating ephemeral key mode. */ + @FlaggedApi(Flags.FLAG_EPSKC_ENABLED) + @NonNull + public Duration getMaxEphemeralKeyLifetime() { + return EPHEMERAL_KEY_LIFETIME_MAX; + } + + /** + * Activates ephemeral key mode with a given {@code lifetime}. The ephemeral key is a temporary, + * single-use numeric code that is used for Thread Administration Sharing. After activation, the + * mode may expire or get deactivated, caller to this method should subscribe to the ephemeral + * key state updates with {@link #registerStateCallback} to get notified when the ephemeral key + * state changes. + * + *

On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. The ephemeral + * key string contains a sequence of numeric digits 0-9 of user-input friendly length (typically + * 9). Subscribers to ephemeral key state updates with {@link #registerStateCallback} will be + * notified with a call to {@link #onEphemeralKeyStateChanged}. + * + *

On failure, {@link OutcomeReceiver#onError} of {@code receiver} will be invoked with a + * specific error: + * + *

    + *
  • {@link ThreadNetworkException#ERROR_FAILED_PRECONDITION} when this device is not + * attached to Thread network + *
  • {@link ThreadNetworkException#ERROR_BUSY} when ephemeral key mode is already activated + * on the device, caller can recover from this error when the ephemeral key mode gets + * deactivated + *
+ * + * @param lifetime valid lifetime of the generated ephemeral key, should be larger than {@link + * Duration#ZERO} and at most the duration returned by {@link #getMaxEphemeralKeyLifetime}. + * @param executor the executor on which to execute {@code receiver} + * @param receiver the receiver to receive the result of this operation + * @throws IllegalArgumentException if the {@code lifetime} exceeds the allowed range + */ + @FlaggedApi(Flags.FLAG_EPSKC_ENABLED) + @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") + public void activateEphemeralKeyMode( + @NonNull Duration lifetime, + @NonNull @CallbackExecutor Executor executor, + @NonNull OutcomeReceiver receiver) { + if (lifetime.compareTo(Duration.ZERO) <= 0 + || lifetime.compareTo(EPHEMERAL_KEY_LIFETIME_MAX) > 0) { + throw new IllegalArgumentException( + "Invalid ephemeral key lifetime: the value must be in range of (0, " + + EPHEMERAL_KEY_LIFETIME_MAX + + "]"); + } + long lifetimeMillis = lifetime.toMillis(); + try { + mControllerService.activateEphemeralKeyMode( + lifetimeMillis, new OperationReceiverProxy(executor, receiver)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Deactivates ephemeral key mode. If there is an active connection with the ephemeral key, the + * connection will be terminated. + * + *

On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. The call will + * always succeed if the device is not in ephemeral key mode. + * + * @param executor the executor to execute {@code receiver} + * @param receiver the receiver to receive the result of this operation + */ + @FlaggedApi(Flags.FLAG_EPSKC_ENABLED) + @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") + public void deactivateEphemeralKeyMode( + @NonNull @CallbackExecutor Executor executor, + @NonNull OutcomeReceiver receiver) { + try { + mControllerService.deactivateEphemeralKeyMode( + new OperationReceiverProxy(executor, receiver)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** Returns the Thread version this device is operating on. */ @ThreadVersion public int getThreadVersion() { @@ -248,6 +359,24 @@ public final class ThreadNetworkController { * @param enabledState the new Thread enabled state */ default void onThreadEnableStateChanged(@EnabledState int enabledState) {} + + /** + * The ephemeral key state has changed. + * + * @param ephemeralKeyState the ephemeral key state + * @param ephemeralKey the ephemeral key string which contains a sequence of numeric digits + * 0-9 of user-input friendly length (typically 9), or {@code null} if {@code + * ephemeralKeyState} is {@link #EPHEMERAL_KEY_DISABLED} or the caller doesn't have the + * permission {@link android.permission.THREAD_NETWORK_PRIVILEGED} + * @param expiry a timestamp of when the ephemeral key will expireor {@code null} if {@code + * ephemeralKeyState} is {@link #EPHEMERAL_KEY_DISABLED} + */ + @FlaggedApi(Flags.FLAG_EPSKC_ENABLED) + @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") + default void onEphemeralKeyStateChanged( + @EphemeralKeyState int ephemeralKeyState, + @Nullable String ephemeralKey, + @Nullable Instant expiry) {} } private static final class StateCallbackProxy extends IStateCallback.Stub { @@ -288,13 +417,37 @@ public final class ThreadNetworkController { Binder.restoreCallingIdentity(identity); } } + + @Override + public void onEphemeralKeyStateChanged( + @EphemeralKeyState int ephemeralKeyState, String ephemeralKey, long expiryMillis) { + if (!Flags.epskcEnabled()) { + throw new IllegalStateException( + "This should not be called when Ephemeral key API is disabled"); + } + + final long identity = Binder.clearCallingIdentity(); + final Instant expiry = + ephemeralKeyState == EPHEMERAL_KEY_DISABLED + ? null + : Instant.ofEpochMilli(expiryMillis); + + try { + mExecutor.execute( + () -> + mCallback.onEphemeralKeyStateChanged( + ephemeralKeyState, ephemeralKey, expiry)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } } /** * Registers a callback to be called when Thread network states are changed. * - *

Upon return of this method, methods of {@code callback} will be invoked immediately with - * existing states. + *

Upon return of this method, all methods of {@code callback} will be invoked immediately + * with existing states. The order of the invoked callbacks is not guaranteed. * * @param executor the executor to execute the {@code callback} * @param callback the callback to receive Thread network state changes diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml index 34aabe27ba..e954d3b42b 100644 --- a/thread/tests/cts/AndroidTest.xml +++ b/thread/tests/cts/AndroidTest.xml @@ -56,4 +56,14 @@