[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:
parent
28617443d2
commit
b8aa8bb47f
7 changed files with 480 additions and 2 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue