[Thread][API] add API for ephemeral key.

This CL adds the API for starting/stopping the ephemeral key mode, and
subscribing to the ephemeral key state changes.

Ephemeral key mode is started with a limit amount of time to allow
Thread credential sharing.

Bug: 348323500
Test: atest CtsThreadNetworkTestCases
Change-Id: I540c88ec4668291358fbe87a678d8f9d31a67e54
This commit is contained in:
Yang Sun 2024-06-20 17:20:49 +08:00
commit b8aa8bb47f
7 changed files with 480 additions and 2 deletions

View file

@ -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"
}

View file

@ -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<java.lang.Void,android.net.thread.ThreadNetworkException>);
method public void createRandomizedDataset(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.thread.ActiveOperationalDataset,android.net.thread.ThreadNetworkException>);
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<java.lang.Void,android.net.thread.ThreadNetworkException>);
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<java.lang.Void,android.net.thread.ThreadNetworkException>);
@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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.
*
* <p>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}.
*
* <p>On failure, {@link OutcomeReceiver#onError} of {@code receiver} will be invoked with a
* specific error:
*
* <ul>
* <li>{@link ThreadNetworkException#ERROR_FAILED_PRECONDITION} when this device is not
* attached to Thread network
* <li>{@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
* </ul>
*
* @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<Void, ThreadNetworkException> 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.
*
* <p>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<Void, ThreadNetworkException> 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.
*
* <p>Upon return of this method, methods of {@code callback} will be invoked immediately with
* existing states.
* <p>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

View file

@ -56,4 +56,14 @@
<!-- Ignores tests introduced by guava-android-testlib -->
<option name="exclude-annotation" value="org.junit.Ignore"/>
</test>
<!--
This doesn't override a read-only flag, to run the tests locally with `epskc_enabled` flag
enabled, set the flag to `is_fixed_read_only: false`. This should be removed after the
`epskc_enabled` flag is rolled out.
-->
<target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer">
<option name="flag-value"
value="thread_network/com.android.net.thread.flags.epskc_enabled=true"/>
</target_preparer>
</configuration>

View file

@ -27,11 +27,14 @@ import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_ROUTER;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
import static android.net.thread.ThreadNetworkController.EPHEMERAL_KEY_DISABLED;
import static android.net.thread.ThreadNetworkController.EPHEMERAL_KEY_ENABLED;
import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_DISABLING;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
import static android.net.thread.ThreadNetworkException.ERROR_ABORTED;
import static android.net.thread.ThreadNetworkException.ERROR_BUSY;
import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER;
import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
@ -72,6 +75,8 @@ import android.os.Build;
import android.os.HandlerThread;
import android.os.OutcomeReceiver;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
@ -82,6 +87,8 @@ import com.android.net.module.util.ArrayTrackRecord;
import com.android.net.thread.flags.Flags;
import com.android.testutils.FunctionalUtils.ThrowingRunnable;
import kotlin.Triple;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
@ -96,6 +103,7 @@ import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
@ -134,9 +142,13 @@ public class ThreadNetworkControllerTest {
put(VALID_CHANNEL, VALID_POWER);
}
};
private static final Duration EPHEMERAL_KEY_LIFETIME = Duration.ofSeconds(1);
@Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
private final Context mContext = ApplicationProvider.getApplicationContext();
private ExecutorService mExecutor;
private ThreadNetworkController mController;
@ -164,6 +176,7 @@ public class ThreadNetworkControllerTest {
setEnabledAndWait(mController, true);
setConfigurationAndWait(mController, DEFAULT_CONFIG);
deactivateEphemeralKeyModeAndWait(mController);
}
@After
@ -183,6 +196,7 @@ public class ThreadNetworkControllerTest {
}
}
mConfigurationCallbacksToCleanUp.clear();
deactivateEphemeralKeyModeAndWait(mController);
}
@Test
@ -819,6 +833,221 @@ public class ThreadNetworkControllerTest {
listener.unregisterStateCallback();
}
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void getMaxEphemeralKeyLifetime_isLargerThanZero() {
assertThat(mController.getMaxEphemeralKeyLifetime()).isGreaterThan(Duration.ZERO);
}
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void activateEphemeralKeyMode_withPrivilegedPermission_succeeds() throws Exception {
joinRandomizedDatasetAndWait(mController);
CompletableFuture<Void> startFuture = new CompletableFuture<>();
runAsShell(
THREAD_NETWORK_PRIVILEGED,
() ->
mController.activateEphemeralKeyMode(
EPHEMERAL_KEY_LIFETIME,
mExecutor,
newOutcomeReceiver(startFuture)));
startFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
}
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void activateEphemeralKeyMode_withoutPrivilegedPermission_throwsSecurityException()
throws Exception {
dropAllPermissions();
assertThrows(
SecurityException.class,
() ->
mController.activateEphemeralKeyMode(
EPHEMERAL_KEY_LIFETIME, mExecutor, v -> {}));
}
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void activateEphemeralKeyMode_withZeroLifetime_throwsIllegalArgumentException()
throws Exception {
grantPermissions(THREAD_NETWORK_PRIVILEGED);
assertThrows(
IllegalArgumentException.class,
() -> mController.activateEphemeralKeyMode(Duration.ZERO, mExecutor, v -> {}));
}
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void activateEphemeralKeyMode_withInvalidLargeLifetime_throwsIllegalArgumentException()
throws Exception {
grantPermissions(THREAD_NETWORK_PRIVILEGED);
Duration lifetime = mController.getMaxEphemeralKeyLifetime().plusMillis(1);
assertThrows(
IllegalArgumentException.class,
() -> mController.activateEphemeralKeyMode(lifetime, Runnable::run, v -> {}));
}
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void activateEphemeralKeyMode_concurrentRequests_secondOneFailsWithBusyError()
throws Exception {
joinRandomizedDatasetAndWait(mController);
CompletableFuture<Void> future1 = new CompletableFuture<>();
CompletableFuture<Void> future2 = new CompletableFuture<>();
runAsShell(
THREAD_NETWORK_PRIVILEGED,
() -> {
mController.activateEphemeralKeyMode(
EPHEMERAL_KEY_LIFETIME, mExecutor, newOutcomeReceiver(future1));
mController.activateEphemeralKeyMode(
EPHEMERAL_KEY_LIFETIME, mExecutor, newOutcomeReceiver(future2));
});
var thrown =
assertThrows(
ExecutionException.class,
() -> {
future2.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
});
var threadException = (ThreadNetworkException) thrown.getCause();
assertThat(threadException.getErrorCode()).isEqualTo(ERROR_BUSY);
}
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void deactivateEphemeralKeyMode_withoutPrivilegedPermission_throwsSecurityException()
throws Exception {
dropAllPermissions();
assertThrows(
SecurityException.class,
() -> mController.deactivateEphemeralKeyMode(mExecutor, v -> {}));
}
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void subscribeEpskcState_permissionsGranted_returnsCurrentState() throws Exception {
CompletableFuture<Integer> stateFuture = new CompletableFuture<>();
CompletableFuture<String> ephemeralKeyFuture = new CompletableFuture<>();
CompletableFuture<Instant> expiryFuture = new CompletableFuture<>();
StateCallback callback =
new ThreadNetworkController.StateCallback() {
@Override
public void onDeviceRoleChanged(int r) {}
@Override
public void onEphemeralKeyStateChanged(
int state, String ephemeralKey, Instant expiry) {
stateFuture.complete(state);
ephemeralKeyFuture.complete(ephemeralKey);
expiryFuture.complete(expiry);
}
};
runAsShell(
ACCESS_NETWORK_STATE,
THREAD_NETWORK_PRIVILEGED,
() -> mController.registerStateCallback(mExecutor, callback));
try {
assertThat(stateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
.isEqualTo(EPHEMERAL_KEY_DISABLED);
assertThat(ephemeralKeyFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
assertThat(expiryFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
} finally {
runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
}
}
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void subscribeEpskcState_withoutThreadPriviledgedPermission_returnsNullEphemeralKey()
throws Exception {
CompletableFuture<Integer> stateFuture = new CompletableFuture<>();
CompletableFuture<String> ephemeralKeyFuture = new CompletableFuture<>();
CompletableFuture<Instant> expiryFuture = new CompletableFuture<>();
StateCallback callback =
new ThreadNetworkController.StateCallback() {
@Override
public void onDeviceRoleChanged(int r) {}
@Override
public void onEphemeralKeyStateChanged(
int state, String ephemeralKey, Instant expiry) {
stateFuture.complete(state);
ephemeralKeyFuture.complete(ephemeralKey);
expiryFuture.complete(expiry);
}
};
joinRandomizedDatasetAndWait(mController);
activateEphemeralKeyModeAndWait(mController);
runAsShell(
ACCESS_NETWORK_STATE, () -> mController.registerStateCallback(mExecutor, callback));
try {
assertThat(stateFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
.isEqualTo(EPHEMERAL_KEY_ENABLED);
assertThat(ephemeralKeyFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)).isNull();
assertThat(
expiryFuture
.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS)
.isAfter(Instant.now()))
.isTrue();
} finally {
runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
}
}
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void subscribeEpskcState_ephemralKeyStateChanged_returnsUpdatedState() throws Exception {
EphemeralKeyStateListener listener = new EphemeralKeyStateListener(mController);
joinRandomizedDatasetAndWait(mController);
try {
activateEphemeralKeyModeAndWait(mController);
deactivateEphemeralKeyModeAndWait(mController);
listener.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_DISABLED);
listener.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_ENABLED);
listener.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_DISABLED);
} finally {
listener.unregisterStateCallback();
}
}
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void subscribeEpskcState_epskcEnabled_returnsSameExpiry() throws Exception {
EphemeralKeyStateListener listener1 = new EphemeralKeyStateListener(mController);
Triple<Integer, String, Instant> epskc1;
try {
joinRandomizedDatasetAndWait(mController);
activateEphemeralKeyModeAndWait(mController);
epskc1 = listener1.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_ENABLED);
} finally {
listener1.unregisterStateCallback();
}
EphemeralKeyStateListener listener2 = new EphemeralKeyStateListener(mController);
try {
Triple<Integer, String, Instant> epskc2 =
listener2.expectThreadEphemeralKeyMode(EPHEMERAL_KEY_ENABLED);
assertThat(epskc2.getSecond()).isEqualTo(epskc1.getSecond());
assertThat(epskc2.getThird()).isEqualTo(epskc1.getThird());
} finally {
listener2.unregisterStateCallback();
}
}
// TODO (b/322437869): add test case to verify when Thread is in DISABLING state, any commands
// (join/leave/scheduleMigration/setEnabled) fail with ERROR_BUSY. This is not currently tested
// because DISABLING has very short lifecycle, it's not possible to guarantee the command can be
@ -1274,6 +1503,71 @@ public class ThreadNetworkControllerTest {
setFuture.get(SET_CONFIGURATION_TIMEOUT_MILLIS, MILLISECONDS);
}
private void deactivateEphemeralKeyModeAndWait(ThreadNetworkController controller)
throws Exception {
CompletableFuture<Void> clearFuture = new CompletableFuture<>();
runAsShell(
THREAD_NETWORK_PRIVILEGED,
() ->
controller.deactivateEphemeralKeyMode(
mExecutor, newOutcomeReceiver(clearFuture)));
clearFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
}
private void activateEphemeralKeyModeAndWait(ThreadNetworkController controller)
throws Exception {
CompletableFuture<Void> startFuture = new CompletableFuture<>();
runAsShell(
THREAD_NETWORK_PRIVILEGED,
() ->
controller.activateEphemeralKeyMode(
EPHEMERAL_KEY_LIFETIME,
mExecutor,
newOutcomeReceiver(startFuture)));
startFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
}
private class EphemeralKeyStateListener {
private ArrayTrackRecord<Triple<Integer, String, Instant>> mEphemeralKeyStates =
new ArrayTrackRecord<>();
private final ArrayTrackRecord<Triple<Integer, String, Instant>>.ReadHead mReadHead =
mEphemeralKeyStates.newReadHead();
ThreadNetworkController mController;
StateCallback mCallback =
new ThreadNetworkController.StateCallback() {
@Override
public void onDeviceRoleChanged(int r) {}
@Override
public void onEphemeralKeyStateChanged(
int state, String ephemeralKey, Instant expiry) {
mEphemeralKeyStates.add(new Triple<>(state, ephemeralKey, expiry));
}
};
EphemeralKeyStateListener(ThreadNetworkController controller) {
this.mController = controller;
runAsShell(
ACCESS_NETWORK_STATE,
THREAD_NETWORK_PRIVILEGED,
() -> controller.registerStateCallback(mExecutor, mCallback));
}
// Expect that EphemeralKey has the expected state, and return a Triple of <state,
// passcode, expiry>.
public Triple<Integer, String, Instant> expectThreadEphemeralKeyMode(int state) {
Triple<Integer, String, Instant> epskc =
mReadHead.poll(
ENABLED_TIMEOUT_MILLIS, e -> Objects.equals(e.getFirst(), state));
assertThat(epskc).isNotNull();
return epskc;
}
public void unregisterStateCallback() {
runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(mCallback));
}
}
private CompletableFuture joinRandomizedDataset(
ThreadNetworkController controller, String networkName) throws Exception {
ActiveOperationalDataset activeDataset = newRandomizedDataset(networkName, controller);