From 7fea5fa6a2a863c356425ac6a036a8392b5d0259 Mon Sep 17 00:00:00 2001 From: Kangping Dong Date: Wed, 30 Aug 2023 17:25:34 +0800 Subject: [PATCH] [Thread] add Thread Operational Dataset API Design doc: go/thread-android-api Android FR: b/235016403 Bug: 262683651 Change-Id: Icbd4ee4150e3fd78df627c2e726c259e7ee50871 --- framework-t/api/system-current.txt | 75 ++ .../net/thread/ActiveOperationalDataset.aidl | 19 + .../net/thread/ActiveOperationalDataset.java | 1165 +++++++++++++++++ .../thread/OperationalDatasetTimestamp.java | 220 ++++ .../net/thread/PendingOperationalDataset.aidl | 19 + .../net/thread/PendingOperationalDataset.java | 226 ++++ thread/tests/cts/Android.bp | 1 + thread/tests/cts/AndroidTest.xml | 2 + .../cts/ActiveOperationalDatasetTest.java | 737 +++++++++++ .../cts/OperationalDatasetTimestampTest.java | 127 ++ .../cts/PendingOperationalDatasetTest.java | 247 ++++ thread/tests/unit/Android.bp | 50 + thread/tests/unit/AndroidManifest.xml | 30 + thread/tests/unit/AndroidTest.xml | 33 + .../thread/ActiveOperationalDatasetTest.java | 202 +++ .../OperationalDatasetTimestampTest.java | 61 + 16 files changed, 3214 insertions(+) create mode 100644 thread/framework/java/android/net/thread/ActiveOperationalDataset.aidl create mode 100644 thread/framework/java/android/net/thread/ActiveOperationalDataset.java create mode 100644 thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java create mode 100644 thread/framework/java/android/net/thread/PendingOperationalDataset.aidl create mode 100644 thread/framework/java/android/net/thread/PendingOperationalDataset.java create mode 100644 thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java create mode 100644 thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java create mode 100644 thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java create mode 100644 thread/tests/unit/Android.bp create mode 100644 thread/tests/unit/AndroidManifest.xml create mode 100644 thread/tests/unit/AndroidTest.xml create mode 100644 thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java create mode 100644 thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt index 06d3238f1b..6c98a4fc73 100644 --- a/framework-t/api/system-current.txt +++ b/framework-t/api/system-current.txt @@ -417,6 +417,81 @@ package android.net.nsd { package android.net.thread { + @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ActiveOperationalDataset implements android.os.Parcelable { + method @NonNull public static android.net.thread.ActiveOperationalDataset createRandomDataset(); + method public int describeContents(); + method @NonNull public static android.net.thread.ActiveOperationalDataset fromThreadTlvs(@NonNull byte[]); + method @NonNull public android.net.thread.OperationalDatasetTimestamp getActiveTimestamp(); + method @IntRange(from=0, to=65535) public int getChannel(); + method @NonNull @Size(min=1) public android.util.SparseArray getChannelMask(); + method @IntRange(from=0, to=255) public int getChannelPage(); + method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID) public byte[] getExtendedPanId(); + method @NonNull public android.net.IpPrefix getMeshLocalPrefix(); + method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_NETWORK_KEY) public byte[] getNetworkKey(); + method @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.LENGTH_MIN_NETWORK_NAME_BYTES, max=android.net.thread.ActiveOperationalDataset.LENGTH_MAX_NETWORK_NAME_BYTES) public String getNetworkName(); + method @IntRange(from=0, to=65534) public int getPanId(); + method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_PSKC) public byte[] getPskc(); + method @NonNull public android.net.thread.ActiveOperationalDataset.SecurityPolicy getSecurityPolicy(); + method @NonNull public byte[] toThreadTlvs(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field public static final int CHANNEL_MAX_24_GHZ = 26; // 0x1a + field public static final int CHANNEL_MIN_24_GHZ = 11; // 0xb + field public static final int CHANNEL_PAGE_24_GHZ = 0; // 0x0 + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + field public static final int LENGTH_EXTENDED_PAN_ID = 8; // 0x8 + field public static final int LENGTH_MAX_DATASET_TLVS = 254; // 0xfe + field public static final int LENGTH_MAX_NETWORK_NAME_BYTES = 16; // 0x10 + field public static final int LENGTH_MESH_LOCAL_PREFIX_BITS = 64; // 0x40 + field public static final int LENGTH_MIN_NETWORK_NAME_BYTES = 1; // 0x1 + field public static final int LENGTH_NETWORK_KEY = 16; // 0x10 + field public static final int LENGTH_PSKC = 16; // 0x10 + } + + public static final class ActiveOperationalDataset.Builder { + ctor public ActiveOperationalDataset.Builder(@NonNull android.net.thread.ActiveOperationalDataset); + ctor public ActiveOperationalDataset.Builder(); + method @NonNull public android.net.thread.ActiveOperationalDataset build(); + method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setActiveTimestamp(@NonNull android.net.thread.OperationalDatasetTimestamp); + method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setChannel(@IntRange(from=0, to=255) int, @IntRange(from=0, to=65535) int); + method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setChannelMask(@NonNull @Size(min=1) android.util.SparseArray); + method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setExtendedPanId(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID) byte[]); + method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setMeshLocalPrefix(@NonNull android.net.IpPrefix); + method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setNetworkKey(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_NETWORK_KEY) byte[]); + method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setNetworkName(@NonNull @Size(min=android.net.thread.ActiveOperationalDataset.LENGTH_MIN_NETWORK_NAME_BYTES, max=android.net.thread.ActiveOperationalDataset.LENGTH_MAX_NETWORK_NAME_BYTES) String); + method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setPanId(@IntRange(from=0, to=65534) int); + method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setPskc(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_PSKC) byte[]); + method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setSecurityPolicy(@NonNull android.net.thread.ActiveOperationalDataset.SecurityPolicy); + } + + public static final class ActiveOperationalDataset.SecurityPolicy { + ctor public ActiveOperationalDataset.SecurityPolicy(@IntRange(from=1, to=65535) int, @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.SecurityPolicy.LENGTH_MIN_SECURITY_POLICY_FLAGS) byte[]); + method @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.SecurityPolicy.LENGTH_MIN_SECURITY_POLICY_FLAGS) public byte[] getFlags(); + method @IntRange(from=1, to=65535) public int getRotationTimeHours(); + field public static final int DEFAULT_ROTATION_TIME_HOURS = 672; // 0x2a0 + field public static final int LENGTH_MIN_SECURITY_POLICY_FLAGS = 1; // 0x1 + } + + @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class OperationalDatasetTimestamp { + ctor public OperationalDatasetTimestamp(@IntRange(from=0, to=281474976710655L) long, @IntRange(from=0, to=32767) int, boolean); + method @NonNull public static android.net.thread.OperationalDatasetTimestamp fromInstant(@NonNull java.time.Instant); + method @IntRange(from=0, to=281474976710655L) public long getSeconds(); + method @IntRange(from=0, to=32767) public int getTicks(); + method public boolean isAuthoritativeSource(); + method @NonNull public java.time.Instant toInstant(); + } + + @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class PendingOperationalDataset implements android.os.Parcelable { + ctor public PendingOperationalDataset(@NonNull android.net.thread.ActiveOperationalDataset, @NonNull android.net.thread.OperationalDatasetTimestamp, @NonNull java.time.Duration); + method public int describeContents(); + method @NonNull public static android.net.thread.PendingOperationalDataset fromThreadTlvs(@NonNull byte[]); + method @NonNull public android.net.thread.ActiveOperationalDataset getActiveOperationalDataset(); + method @NonNull public java.time.Duration getDelayTimer(); + method @NonNull public android.net.thread.OperationalDatasetTimestamp getPendingTimestamp(); + method @NonNull public byte[] toThreadTlvs(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + @FlaggedApi("com.android.net.thread.flags.thread_enabled") public class ThreadNetworkController { method public int getThreadVersion(); field public static final int THREAD_VERSION_1_3 = 4; // 0x4 diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.aidl b/thread/framework/java/android/net/thread/ActiveOperationalDataset.aidl new file mode 100644 index 0000000000..8bf12a428e --- /dev/null +++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.thread; + +parcelable ActiveOperationalDataset; diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java new file mode 100644 index 0000000000..c9b047ae77 --- /dev/null +++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java @@ -0,0 +1,1165 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.thread; + +import static android.net.thread.ActiveOperationalDataset.SecurityPolicy.DEFAULT_ROTATION_TIME_HOURS; + +import static com.android.internal.util.Preconditions.checkArgument; +import static com.android.internal.util.Preconditions.checkState; +import static com.android.net.module.util.HexDump.dumpHexString; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +import android.annotation.FlaggedApi; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.Size; +import android.annotation.SystemApi; +import android.net.IpPrefix; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.SparseArray; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.ByteArrayOutputStream; +import java.net.Inet6Address; +import java.net.UnknownHostException; +import java.security.SecureRandom; +import java.time.Instant; +import java.util.Arrays; +import java.util.Random; + +/** + * Data interface for managing a Thread Active Operational Dataset. + * + *

An example usage of creating an Active Operational Dataset with random parameters: + * + *

{@code
+ * ActiveOperationalDataset activeDataset = ActiveOperationalDataset.createRandomDataset();
+ * }
+ * + *

or random Dataset with customized Network Name: + * + *

{@code
+ * ActiveOperationalDataset activeDataset =
+ *         new ActiveOperationalDataset.Builder(ActiveOperationalDataset.createRandomDataset())
+ *                 .setNetworkName("MyThreadNet").build();
+ * }
+ * + *

If the Active Operational Dataset is already known as Thread TLVs, you can simply use: + * + *

{@code
+ * ActiveOperationalDataset activeDataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlvs);
+ * }
+ * + * @hide + */ +@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED) +@SystemApi +public final class ActiveOperationalDataset implements Parcelable { + /** The maximum length of the Active Operational Dataset TLV array in bytes. */ + public static final int LENGTH_MAX_DATASET_TLVS = 254; + /** The length of Extended PAN ID in bytes. */ + public static final int LENGTH_EXTENDED_PAN_ID = 8; + /** The minimum length of Network Name as UTF-8 bytes. */ + public static final int LENGTH_MIN_NETWORK_NAME_BYTES = 1; + /** The maximum length of Network Name as UTF-8 bytes. */ + public static final int LENGTH_MAX_NETWORK_NAME_BYTES = 16; + /** The length of Network Key in bytes. */ + public static final int LENGTH_NETWORK_KEY = 16; + /** The length of Mesh-Local Prefix in bits. */ + public static final int LENGTH_MESH_LOCAL_PREFIX_BITS = 64; + /** The length of PSKc in bytes. */ + public static final int LENGTH_PSKC = 16; + /** The 2.4 GHz channel page. */ + public static final int CHANNEL_PAGE_24_GHZ = 0; + /** The minimum 2.4GHz channel. */ + public static final int CHANNEL_MIN_24_GHZ = 11; + /** The maximum 2.4GHz channel. */ + public static final int CHANNEL_MAX_24_GHZ = 26; + /** @hide */ + @VisibleForTesting public static final int TYPE_CHANNEL = 0; + /** @hide */ + @VisibleForTesting public static final int TYPE_PAN_ID = 1; + /** @hide */ + @VisibleForTesting public static final int TYPE_EXTENDED_PAN_ID = 2; + /** @hide */ + @VisibleForTesting public static final int TYPE_NETWORK_NAME = 3; + /** @hide */ + @VisibleForTesting public static final int TYPE_PSKC = 4; + /** @hide */ + @VisibleForTesting public static final int TYPE_NETWORK_KEY = 5; + /** @hide */ + @VisibleForTesting public static final int TYPE_MESH_LOCAL_PREFIX = 7; + /** @hide */ + @VisibleForTesting public static final int TYPE_SECURITY_POLICY = 12; + /** @hide */ + @VisibleForTesting public static final int TYPE_ACTIVE_TIMESTAMP = 14; + /** @hide */ + @VisibleForTesting public static final int TYPE_CHANNEL_MASK = 53; + + private static final byte MESH_LOCAL_PREFIX_FIRST_BYTE = (byte) 0xfd; + private static final int LENGTH_CHANNEL = 3; + private static final int LENGTH_PAN_ID = 2; + + @NonNull + public static final Creator CREATOR = + new Creator<>() { + @Override + public ActiveOperationalDataset createFromParcel(Parcel in) { + return ActiveOperationalDataset.fromThreadTlvs(in.createByteArray()); + } + + @Override + public ActiveOperationalDataset[] newArray(int size) { + return new ActiveOperationalDataset[size]; + } + }; + + private final OperationalDatasetTimestamp mActiveTimestamp; + private final String mNetworkName; + private final byte[] mExtendedPanId; + private final int mPanId; + private final int mChannel; + private final int mChannelPage; + private final SparseArray mChannelMask; + private final byte[] mPskc; + private final byte[] mNetworkKey; + private final IpPrefix mMeshLocalPrefix; + private final SecurityPolicy mSecurityPolicy; + private final SparseArray mUnknownTlvs; + + private ActiveOperationalDataset(Builder builder) { + this( + requireNonNull(builder.mActiveTimestamp), + requireNonNull(builder.mNetworkName), + requireNonNull(builder.mExtendedPanId), + requireNonNull(builder.mPanId), + requireNonNull(builder.mChannelPage), + requireNonNull(builder.mChannel), + requireNonNull(builder.mChannelMask), + requireNonNull(builder.mPskc), + requireNonNull(builder.mNetworkKey), + requireNonNull(builder.mMeshLocalPrefix), + requireNonNull(builder.mSecurityPolicy), + requireNonNull(builder.mUnknownTlvs)); + } + + private ActiveOperationalDataset( + OperationalDatasetTimestamp activeTimestamp, + String networkName, + byte[] extendedPanId, + int panId, + int channelPage, + int channel, + SparseArray channelMask, + byte[] pskc, + byte[] networkKey, + IpPrefix meshLocalPrefix, + SecurityPolicy securityPolicy, + SparseArray unknownTlvs) { + this.mActiveTimestamp = activeTimestamp; + this.mNetworkName = networkName; + this.mExtendedPanId = extendedPanId.clone(); + this.mPanId = panId; + this.mChannel = channel; + this.mChannelPage = channelPage; + this.mChannelMask = deepCloneSparseArray(channelMask); + this.mPskc = pskc.clone(); + this.mNetworkKey = networkKey.clone(); + this.mMeshLocalPrefix = meshLocalPrefix; + this.mSecurityPolicy = securityPolicy; + this.mUnknownTlvs = deepCloneSparseArray(unknownTlvs); + } + + /** + * Creates a new {@link ActiveOperationalDataset} object from a series of Thread TLVs. + * + *

{@code tlvs} can be obtained from the value of a Thread Active Operational Dataset TLV + * (see the Thread + * specification for the definition) or the return value of {@link #toThreadTlvs}. + * + * @param tlvs a series of Thread TLVs which contain the Active Operational Dataset + * @return the decoded Active Operational Dataset + * @throws IllegalArgumentException if {@code tlvs} is malformed or the length is larger than + * {@link LENGTH_MAX_DATASET_TLVS} + */ + @NonNull + public static ActiveOperationalDataset fromThreadTlvs(@NonNull byte[] tlvs) { + requireNonNull(tlvs, "tlvs cannot be null"); + if (tlvs.length > LENGTH_MAX_DATASET_TLVS) { + throw new IllegalArgumentException( + String.format( + "tlvs length exceeds max length %d (actual is %d)", + LENGTH_MAX_DATASET_TLVS, tlvs.length)); + } + + Builder builder = new Builder(); + int i = 0; + while (i < tlvs.length) { + int type = tlvs[i++] & 0xff; + if (i >= tlvs.length) { + throw new IllegalArgumentException( + String.format( + "Found TLV type %d at end of operational dataset with length %d", + type, tlvs.length)); + } + + int length = tlvs[i++] & 0xff; + if (i + length > tlvs.length) { + throw new IllegalArgumentException( + String.format( + "Found TLV type %d with length %d which exceeds the remaining data" + + " in the operational dataset with length %d", + type, length, tlvs.length)); + } + + initWithTlv(builder, type, Arrays.copyOfRange(tlvs, i, i + length)); + i += length; + } + try { + return builder.build(); + } catch (IllegalStateException e) { + throw new IllegalArgumentException( + "Failed to build the ActiveOperationalDataset object", e); + } + } + + private static void initWithTlv(Builder builder, int type, byte[] value) { + // The max length of the dataset is 254 bytes, so the max length of a single TLV value is + // 252 (254 - 1 - 1) + if (value.length > LENGTH_MAX_DATASET_TLVS - 2) { + throw new IllegalArgumentException( + String.format( + "Length of TLV %d exceeds %d (actualLength = %d)", + (type & 0xff), LENGTH_MAX_DATASET_TLVS - 2, value.length)); + } + + switch (type) { + case TYPE_CHANNEL: + checkArgument( + value.length == LENGTH_CHANNEL, + "Invalid channel (length = %d, expectedLength = %d)", + value.length, + LENGTH_CHANNEL); + builder.setChannel((value[0] & 0xff), ((value[1] & 0xff) << 8) | (value[2] & 0xff)); + break; + case TYPE_PAN_ID: + checkArgument( + value.length == LENGTH_PAN_ID, + "Invalid PAN ID (length = %d, expectedLength = %d)", + value.length, + LENGTH_PAN_ID); + builder.setPanId(((value[0] & 0xff) << 8) | (value[1] & 0xff)); + break; + case TYPE_EXTENDED_PAN_ID: + builder.setExtendedPanId(value); + break; + case TYPE_NETWORK_NAME: + builder.setNetworkName(new String(value, UTF_8)); + break; + case TYPE_PSKC: + builder.setPskc(value); + break; + case TYPE_NETWORK_KEY: + builder.setNetworkKey(value); + break; + case TYPE_MESH_LOCAL_PREFIX: + builder.setMeshLocalPrefix(value); + break; + case TYPE_SECURITY_POLICY: + builder.setSecurityPolicy(SecurityPolicy.fromTlvValue(value)); + break; + case TYPE_ACTIVE_TIMESTAMP: + builder.setActiveTimestamp(OperationalDatasetTimestamp.fromTlvValue(value)); + break; + case TYPE_CHANNEL_MASK: + builder.setChannelMask(decodeChannelMask(value)); + break; + default: + builder.addUnknownTlv(type & 0xff, value); + break; + } + } + + private static SparseArray decodeChannelMask(byte[] tlvValue) { + SparseArray channelMask = new SparseArray<>(); + int i = 0; + while (i < tlvValue.length) { + int channelPage = tlvValue[i++] & 0xff; + if (i >= tlvValue.length) { + throw new IllegalArgumentException( + "Invalid channel mask - channel mask length is missing"); + } + + int maskLength = tlvValue[i++] & 0xff; + if (i + maskLength > tlvValue.length) { + throw new IllegalArgumentException( + String.format( + "Invalid channel mask - channel mask is incomplete " + + "(offset = %d, length = %d, totalLength = %d)", + i, maskLength, tlvValue.length)); + } + + channelMask.put(channelPage, Arrays.copyOfRange(tlvValue, i, i + maskLength)); + i += maskLength; + } + return channelMask; + } + + private static void encodeChannelMask( + SparseArray channelMask, ByteArrayOutputStream outputStream) { + ByteArrayOutputStream entryStream = new ByteArrayOutputStream(); + + for (int i = 0; i < channelMask.size(); i++) { + int key = channelMask.keyAt(i); + byte[] value = channelMask.get(key); + entryStream.write(key); + entryStream.write(value.length); + entryStream.write(value, 0, value.length); + } + + byte[] entries = entryStream.toByteArray(); + + outputStream.write(TYPE_CHANNEL_MASK); + outputStream.write(entries.length); + outputStream.write(entries, 0, entries.length); + } + + /** + * Creates a new {@link ActiveOperationalDataset} object with randomized or default parameters. + * + *

The randomized (or default) value for each parameter: + * + *

    + *
  • {@code Active Timestamp} defaults to {@code new OperationalDatasetTimestamp(1, 0, + * false)} + *
  • {@code Network Name} defaults to "THREAD-PAN-", for example + * "THREAD-PAN-12345" + *
  • {@code Extended PAN ID} filled with randomly generated bytes + *
  • {@code PAN ID} randomly generated integer in range of [0, 0xfffe] + *
  • {@code Channel Page} defaults to {@link #CHANNEL_PAGE_24_GHZ} + *
  • {@code Channel} randomly selected channel in range of [{@link #CHANNEL_MIN_24_GHZ}, + * {@link #CHANNEL_MAX_24_GHZ}] + *
  • {@code Channel Mask} all bits from {@link #CHANNEL_MIN_24_GHZ} to {@link + * #CHANNEL_MAX_24_GHZ} are set to {@code true} + *
  • {@code PSKc} filled with bytes generated by secure random generator + *
  • {@code Network Key} filled with bytes generated by secure random generator + *
  • {@code Mesh-local Prefix} filled with randomly generated bytes except that the first + * byte is always set to {@code 0xfd} + *
  • {@code Security Policy} defaults to {@code new SecurityPolicy( + * DEFAULT_ROTATION_TIME_HOURS, new byte[]{(byte)0xff, (byte)0xf8})}. This is the default + * values required by the Thread 1.2 specification + *
+ * + *

This method is the recommended way to create a randomized operational dataset for a new + * Thread network. It may be desired to change one or more of the generated value(s). For + * example, to use a more meaningful Network Name. To do that, create a new {@link Builder} + * object from this dataset with {@link Builder#Builder(ActiveOperationalDataset)} and override + * the value with the setters of {@link Builder}. + * + *

Note that it's highly discouraged to change the randomly generated Extended PAN ID, + * Network Key or PSKc, as it will compromise the security of a Thread network. + */ + @NonNull + public static ActiveOperationalDataset createRandomDataset() { + return createRandomDataset(new Random(Instant.now().toEpochMilli()), new SecureRandom()); + } + + /** @hide */ + @VisibleForTesting + public static ActiveOperationalDataset createRandomDataset( + Random random, SecureRandom secureRandom) { + int panId = random.nextInt(/* bound= */ 0xffff); + byte[] meshLocalPrefix = newRandomBytes(random, LENGTH_MESH_LOCAL_PREFIX_BITS / 8); + meshLocalPrefix[0] = MESH_LOCAL_PREFIX_FIRST_BYTE; + + SparseArray channelMask = new SparseArray<>(1); + channelMask.put(CHANNEL_PAGE_24_GHZ, new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0}); + + return new Builder() + .setActiveTimestamp( + new OperationalDatasetTimestamp( + /* seconds= */ 1, + /* ticks= */ 0, + /* isAuthoritativeSource= */ false)) + .setExtendedPanId(newRandomBytes(random, LENGTH_EXTENDED_PAN_ID)) + .setPanId(panId) + .setNetworkName("THREAD-PAN-" + panId) + .setChannel( + CHANNEL_PAGE_24_GHZ, + random.nextInt(CHANNEL_MAX_24_GHZ - CHANNEL_MIN_24_GHZ + 1) + + CHANNEL_MIN_24_GHZ) + .setChannelMask(channelMask) + .setPskc(newRandomBytes(secureRandom, LENGTH_PSKC)) + .setNetworkKey(newRandomBytes(secureRandom, LENGTH_NETWORK_KEY)) + .setMeshLocalPrefix(meshLocalPrefix) + .setSecurityPolicy( + new SecurityPolicy( + DEFAULT_ROTATION_TIME_HOURS, new byte[] {(byte) 0xff, (byte) 0xf8})) + .build(); + } + + private static byte[] newRandomBytes(Random random, int length) { + byte[] result = new byte[length]; + random.nextBytes(result); + return result; + } + + private static boolean areByteSparseArraysEqual( + @NonNull SparseArray first, @NonNull SparseArray second) { + if (first == second) { + return true; + } else if (first == null || second == null) { + return false; + } else if (first.size() != second.size()) { + return false; + } else { + for (int i = 0; i < first.size(); i++) { + int firstKey = first.keyAt(i); + int secondKey = second.keyAt(i); + if (firstKey != secondKey) { + return false; + } + + byte[] firstValue = first.valueAt(i); + byte[] secondValue = second.valueAt(i); + if (!Arrays.equals(firstValue, secondValue)) { + return false; + } + } + return true; + } + } + + /** An easy-to-use wrapper of {@link Arrays#deepHashCode}. */ + private static int deepHashCode(Object... values) { + return Arrays.deepHashCode(values); + } + + /** + * Converts this {@link ActiveOperationalDataset} object to a series of Thread TLVs. + * + *

See the Thread + * specification for the definition of the Thread TLV format. + * + * @return a series of Thread TLVs which contain this Active Operational Dataset + */ + @NonNull + public byte[] toThreadTlvs() { + ByteArrayOutputStream dataset = new ByteArrayOutputStream(); + + dataset.write(TYPE_ACTIVE_TIMESTAMP); + byte[] activeTimestampBytes = mActiveTimestamp.toTlvValue(); + dataset.write(activeTimestampBytes.length); + dataset.write(activeTimestampBytes, 0, activeTimestampBytes.length); + + dataset.write(TYPE_NETWORK_NAME); + byte[] networkNameBytes = mNetworkName.getBytes(UTF_8); + dataset.write(networkNameBytes.length); + dataset.write(networkNameBytes, 0, networkNameBytes.length); + + dataset.write(TYPE_EXTENDED_PAN_ID); + dataset.write(mExtendedPanId.length); + dataset.write(mExtendedPanId, 0, mExtendedPanId.length); + + dataset.write(TYPE_PAN_ID); + dataset.write(LENGTH_PAN_ID); + dataset.write(mPanId >> 8); + dataset.write(mPanId); + + dataset.write(TYPE_CHANNEL); + dataset.write(LENGTH_CHANNEL); + dataset.write(mChannelPage); + dataset.write(mChannel >> 8); + dataset.write(mChannel); + + encodeChannelMask(mChannelMask, dataset); + + dataset.write(TYPE_PSKC); + dataset.write(mPskc.length); + dataset.write(mPskc, 0, mPskc.length); + + dataset.write(TYPE_NETWORK_KEY); + dataset.write(mNetworkKey.length); + dataset.write(mNetworkKey, 0, mNetworkKey.length); + + dataset.write(TYPE_MESH_LOCAL_PREFIX); + dataset.write(mMeshLocalPrefix.getPrefixLength() / 8); + dataset.write(mMeshLocalPrefix.getRawAddress(), 0, mMeshLocalPrefix.getPrefixLength() / 8); + + dataset.write(TYPE_SECURITY_POLICY); + byte[] securityPolicyBytes = mSecurityPolicy.toTlvValue(); + dataset.write(securityPolicyBytes.length); + dataset.write(securityPolicyBytes, 0, securityPolicyBytes.length); + + for (int i = 0; i < mUnknownTlvs.size(); i++) { + byte[] value = mUnknownTlvs.valueAt(i); + dataset.write(mUnknownTlvs.keyAt(i)); + dataset.write(value.length); + dataset.write(value, 0, value.length); + } + + return dataset.toByteArray(); + } + + /** Returns the Active Timestamp. */ + @NonNull + public OperationalDatasetTimestamp getActiveTimestamp() { + return mActiveTimestamp; + } + + /** Returns the Network Name. */ + @NonNull + @Size(min = LENGTH_MIN_NETWORK_NAME_BYTES, max = LENGTH_MAX_NETWORK_NAME_BYTES) + public String getNetworkName() { + return mNetworkName; + } + + /** Returns the Extended PAN ID. */ + @NonNull + @Size(LENGTH_EXTENDED_PAN_ID) + public byte[] getExtendedPanId() { + return mExtendedPanId.clone(); + } + + /** Returns the PAN ID. */ + @IntRange(from = 0, to = 0xfffe) + public int getPanId() { + return mPanId; + } + + /** Returns the Channel. */ + @IntRange(from = 0, to = 65535) + public int getChannel() { + return mChannel; + } + + /** Returns the Channel Page. */ + @IntRange(from = 0, to = 255) + public int getChannelPage() { + return mChannelPage; + } + + /** + * Returns the Channel masks. For the returned {@link SparseArray}, the key is the Channel Page + * and the value is the Channel Mask. + */ + @NonNull + @Size(min = 1) + public SparseArray getChannelMask() { + return deepCloneSparseArray(mChannelMask); + } + + private static SparseArray deepCloneSparseArray(SparseArray src) { + SparseArray dst = new SparseArray<>(src.size()); + for (int i = 0; i < src.size(); i++) { + dst.put(src.keyAt(i), src.valueAt(i).clone()); + } + return dst; + } + + /** Returns the PSKc. */ + @NonNull + @Size(LENGTH_PSKC) + public byte[] getPskc() { + return mPskc.clone(); + } + + /** Returns the Network Key. */ + @NonNull + @Size(LENGTH_NETWORK_KEY) + public byte[] getNetworkKey() { + return mNetworkKey.clone(); + } + + /** + * Returns the Mesh-local Prefix. The length of the returned prefix is always {@link + * #LENGTH_MESH_LOCAL_PREFIX_BITS}. + */ + @NonNull + public IpPrefix getMeshLocalPrefix() { + return mMeshLocalPrefix; + } + + /** Returns the Security Policy. */ + @NonNull + public SecurityPolicy getSecurityPolicy() { + return mSecurityPolicy; + } + + /** + * Returns Thread TLVs which are not recognized by this device. The returned {@link SparseArray} + * associates TLV values to their keys. + * + * @hide + */ + @NonNull + public SparseArray getUnknownTlvs() { + return deepCloneSparseArray(mUnknownTlvs); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeByteArray(toThreadTlvs()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (!(other instanceof ActiveOperationalDataset)) { + return false; + } else { + ActiveOperationalDataset otherDataset = (ActiveOperationalDataset) other; + return mActiveTimestamp.equals(otherDataset.mActiveTimestamp) + && mNetworkName.equals(otherDataset.mNetworkName) + && Arrays.equals(mExtendedPanId, otherDataset.mExtendedPanId) + && mPanId == otherDataset.mPanId + && mChannelPage == otherDataset.mChannelPage + && mChannel == otherDataset.mChannel + && areByteSparseArraysEqual(mChannelMask, otherDataset.mChannelMask) + && Arrays.equals(mPskc, otherDataset.mPskc) + && Arrays.equals(mNetworkKey, otherDataset.mNetworkKey) + && mMeshLocalPrefix.equals(otherDataset.mMeshLocalPrefix) + && mSecurityPolicy.equals(otherDataset.mSecurityPolicy) + && areByteSparseArraysEqual(mUnknownTlvs, otherDataset.mUnknownTlvs); + } + } + + @Override + public int hashCode() { + return deepHashCode( + mActiveTimestamp, + mNetworkName, + mExtendedPanId, + mPanId, + mChannel, + mChannelPage, + mChannelMask, + mPskc, + mNetworkKey, + mMeshLocalPrefix, + mSecurityPolicy); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{networkName=") + .append(getNetworkName()) + .append(", extendedPanId=") + .append(dumpHexString(getExtendedPanId())) + .append(", panId=") + .append(getPanId()) + .append(", channel=") + .append(getChannel()) + .append(", activeTimestamp=") + .append(getActiveTimestamp()) + .append("}"); + return sb.toString(); + } + + /** The builder for creating {@link ActiveOperationalDataset} objects. */ + public static final class Builder { + private OperationalDatasetTimestamp mActiveTimestamp; + private String mNetworkName; + private byte[] mExtendedPanId; + private Integer mPanId; + private Integer mChannel; + private Integer mChannelPage; + private SparseArray mChannelMask; + private byte[] mPskc; + private byte[] mNetworkKey; + private IpPrefix mMeshLocalPrefix; + private SecurityPolicy mSecurityPolicy; + private SparseArray mUnknownTlvs; + + /** + * Creates a {@link Builder} object with values from an {@link ActiveOperationalDataset} + * object. + */ + public Builder(@NonNull ActiveOperationalDataset activeOpDataset) { + requireNonNull(activeOpDataset, "activeOpDataset cannot be null"); + + this.mActiveTimestamp = activeOpDataset.mActiveTimestamp; + this.mNetworkName = activeOpDataset.mNetworkName; + this.mExtendedPanId = activeOpDataset.mExtendedPanId.clone(); + this.mPanId = activeOpDataset.mPanId; + this.mChannel = activeOpDataset.mChannel; + this.mChannelPage = activeOpDataset.mChannelPage; + this.mChannelMask = deepCloneSparseArray(activeOpDataset.mChannelMask); + this.mPskc = activeOpDataset.mPskc.clone(); + this.mNetworkKey = activeOpDataset.mNetworkKey.clone(); + this.mMeshLocalPrefix = activeOpDataset.mMeshLocalPrefix; + this.mSecurityPolicy = activeOpDataset.mSecurityPolicy; + this.mUnknownTlvs = deepCloneSparseArray(activeOpDataset.mUnknownTlvs); + } + + /** + * Creates an empty {@link Builder} object. + * + *

An empty builder cannot build a new {@link ActiveOperationalDataset} object. The + * Active Operational Dataset parameters must be set with setters of this builder. + */ + public Builder() { + mChannelMask = new SparseArray<>(); + mUnknownTlvs = new SparseArray<>(); + } + + /** + * Sets the Active Timestamp. + * + * @param activeTimestamp Active Timestamp of the Operational Dataset + */ + @NonNull + public Builder setActiveTimestamp(@NonNull OperationalDatasetTimestamp activeTimestamp) { + requireNonNull(activeTimestamp, "activeTimestamp cannot be null"); + this.mActiveTimestamp = activeTimestamp; + return this; + } + + /** + * Sets the Network Name. + * + * @param networkName the name of the Thread network + * @throws IllegalArgumentException if length of the UTF-8 representation of {@code + * networkName} isn't in range of [{@link #LENGTH_MIN_NETWORK_NAME_BYTES}, {@link + * #LENGTH_MAX_NETWORK_NAME_BYTES}]. + */ + @NonNull + public Builder setNetworkName( + @NonNull + @Size( + min = LENGTH_MIN_NETWORK_NAME_BYTES, + max = LENGTH_MAX_NETWORK_NAME_BYTES) + String networkName) { + requireNonNull(networkName, "networkName cannot be null"); + + int nameLength = networkName.getBytes(UTF_8).length; + checkArgument( + nameLength >= LENGTH_MIN_NETWORK_NAME_BYTES + && nameLength <= LENGTH_MAX_NETWORK_NAME_BYTES, + "Invalid network name (length = %d, expectedLengthRange = [%d, %d])", + nameLength, + LENGTH_MIN_NETWORK_NAME_BYTES, + LENGTH_MAX_NETWORK_NAME_BYTES); + this.mNetworkName = networkName; + return this; + } + + /** + * Sets the Extended PAN ID. + * + *

Use with caution. A randomly generated Extended PAN ID should be used for real Thread + * networks. It's discouraged to call this method to override the default value created by + * {@link ActiveOperationalDataset#createRandomDataset} in production. + * + * @throws IllegalArgumentException if length of {@code extendedPanId} is not {@link + * #LENGTH_EXTENDED_PAN_ID}. + */ + @NonNull + public Builder setExtendedPanId( + @NonNull @Size(LENGTH_EXTENDED_PAN_ID) byte[] extendedPanId) { + requireNonNull(extendedPanId, "extendedPanId cannot be null"); + checkArgument( + extendedPanId.length == LENGTH_EXTENDED_PAN_ID, + "Invalid extended PAN ID (length = %d, expectedLength = %d)", + extendedPanId.length, + LENGTH_EXTENDED_PAN_ID); + this.mExtendedPanId = extendedPanId.clone(); + return this; + } + + /** + * Sets the PAN ID. + * + * @throws IllegalArgumentException if {@code panId} is not in range of 0x0-0xfffe + */ + @NonNull + public Builder setPanId(@IntRange(from = 0, to = 0xfffe) int panId) { + checkArgument( + panId >= 0 && panId <= 0xfffe, + "PAN ID exceeds allowed range (panid = %d, allowedRange = [0x0, 0xffff])", + panId); + this.mPanId = panId; + return this; + } + + /** + * Sets the Channel Page and Channel. + * + *

Channel Pages other than {@link #CHANNEL_PAGE_24_GHZ} are undefined and may lead to + * unexpected behavior if it's applied to Thread devices. + * + * @throws IllegalArgumentException if invalid channel is specified for the {@code + * channelPage} + */ + @NonNull + public Builder setChannel( + @IntRange(from = 0, to = 255) int page, + @IntRange(from = 0, to = 65535) int channel) { + checkArgument( + page >= 0 && page <= 255, + "Invalid channel page (page = %d, allowedRange = [0, 255])", + page); + if (page == CHANNEL_PAGE_24_GHZ) { + checkArgument( + channel >= CHANNEL_MIN_24_GHZ && channel <= CHANNEL_MAX_24_GHZ, + "Invalid channel %d in page %d (allowedChannelRange = [%d, %d])", + channel, + page, + CHANNEL_MIN_24_GHZ, + CHANNEL_MAX_24_GHZ); + } else { + checkArgument( + channel >= 0 && channel <= 65535, + "Invalid channel %d in page %d " + + "(channel = %d, allowedChannelRange = [0, 65535])", + channel, + page, + channel); + } + + this.mChannelPage = page; + this.mChannel = channel; + return this; + } + + /** + * Sets the Channel Mask. + * + * @throws IllegalArgumentException if {@code channelMask} is empty + */ + @NonNull + public Builder setChannelMask(@NonNull @Size(min = 1) SparseArray channelMask) { + requireNonNull(channelMask, "channelMask cannot be null"); + checkArgument(channelMask.size() > 0, "channelMask is empty"); + this.mChannelMask = deepCloneSparseArray(channelMask); + return this; + } + + /** + * Sets the PSKc. + * + *

Use with caution. A randomly generated PSKc should be used for real Thread networks. + * It's discouraged to call this method to override the default value created by {@link + * ActiveOperationalDataset#createRandomDataset} in production. + * + * @param pskc the key stretched version of the Commissioning Credential for the network + * @throws IllegalArgumentException if length of {@code pskc} is not {@link #LENGTH_PSKC} + */ + @NonNull + public Builder setPskc(@NonNull @Size(LENGTH_PSKC) byte[] pskc) { + requireNonNull(pskc, "pskc cannot be null"); + checkArgument( + pskc.length == LENGTH_PSKC, + "Invalid PSKc length (length = %d, expectedLength = %d)", + pskc.length, + LENGTH_PSKC); + this.mPskc = pskc.clone(); + return this; + } + + /** + * Sets the Network Key. + * + *

Use with caution, randomly generated Network Key should be used for real Thread + * networks. It's discouraged to call this method to override the default value created by + * {@link ActiveOperationalDataset#createRandomDataset} in production. + * + * @param networkKey a 128-bit security key-derivation key for the Thread Network + * @throws IllegalArgumentException if length of {@code networkKey} is not {@link + * #LENGTH_NETWORK_KEY} + */ + @NonNull + public Builder setNetworkKey(@NonNull @Size(LENGTH_NETWORK_KEY) byte[] networkKey) { + requireNonNull(networkKey, "networkKey cannot be null"); + checkArgument( + networkKey.length == LENGTH_NETWORK_KEY, + "Invalid network key length (length = %d, expectedLength = %d)", + networkKey.length, + LENGTH_NETWORK_KEY); + this.mNetworkKey = networkKey.clone(); + return this; + } + + /** + * Sets the Mesh-Local Prefix. + * + * @param meshLocalPrefix the prefix used for realm-local traffic within the mesh + * @throws IllegalArgumentException if prefix length of {@code meshLocalPrefix} isn't {@link + * #LENGTH_MESH_LOCAL_PREFIX_BITS} or {@code meshLocalPrefix} doesn't start with {@code + * 0xfd} + */ + @NonNull + public Builder setMeshLocalPrefix(@NonNull IpPrefix meshLocalPrefix) { + requireNonNull(meshLocalPrefix, "meshLocalPrefix cannot be null"); + checkArgument( + meshLocalPrefix.getPrefixLength() == LENGTH_MESH_LOCAL_PREFIX_BITS, + "Invalid mesh-local prefix length (length = %d, expectedLength = %d)", + meshLocalPrefix.getPrefixLength(), + LENGTH_MESH_LOCAL_PREFIX_BITS); + checkArgument( + meshLocalPrefix.getRawAddress()[0] == MESH_LOCAL_PREFIX_FIRST_BYTE, + "Mesh-local prefix must start with 0xfd: " + meshLocalPrefix); + this.mMeshLocalPrefix = meshLocalPrefix; + return this; + } + + @NonNull + private Builder setMeshLocalPrefix(byte[] meshLocalPrefix) { + final int prefixLength = meshLocalPrefix.length * 8; + checkArgument( + prefixLength == LENGTH_MESH_LOCAL_PREFIX_BITS, + "Invalid mesh-local prefix length (length = %d, expectedLength = %d)", + prefixLength, + LENGTH_MESH_LOCAL_PREFIX_BITS); + byte[] ip6RawAddress = new byte[16]; + System.arraycopy(meshLocalPrefix, 0, ip6RawAddress, 0, meshLocalPrefix.length); + try { + return setMeshLocalPrefix( + new IpPrefix(Inet6Address.getByAddress(ip6RawAddress), prefixLength)); + } catch (UnknownHostException e) { + // Can't happen because numeric address is provided + throw new AssertionError(e); + } + } + + /** Sets the Security Policy. */ + @NonNull + public Builder setSecurityPolicy(@NonNull SecurityPolicy securityPolicy) { + requireNonNull(securityPolicy, "securityPolicy cannot be null"); + this.mSecurityPolicy = securityPolicy; + return this; + } + + /** + * Sets additional unknown TLVs. + * + * @hide + */ + @NonNull + public Builder setUnknownTlvs(@NonNull SparseArray unknownTlvs) { + requireNonNull(unknownTlvs, "unknownTlvs cannot be null"); + mUnknownTlvs = deepCloneSparseArray(unknownTlvs); + return this; + } + + /** Adds one more unknown TLV. @hide */ + @VisibleForTesting + @NonNull + public Builder addUnknownTlv(int type, byte[] value) { + mUnknownTlvs.put(type, value); + return this; + } + + /** + * Creates a new {@link ActiveOperationalDataset} object. + * + * @throws IllegalStateException if any of the fields isn't set or the total length exceeds + * {@link #LENGTH_MAX_DATASET_TLVS} bytes + */ + @NonNull + public ActiveOperationalDataset build() { + checkState(mActiveTimestamp != null, "Active Timestamp is missing"); + checkState(mNetworkName != null, "Network Name is missing"); + checkState(mExtendedPanId != null, "Extended PAN ID is missing"); + checkState(mPanId != null, "PAN ID is missing"); + checkState(mChannel != null, "Channel is missing"); + checkState(mChannelPage != null, "Channel Page is missing"); + checkState(mChannelMask.size() != 0, "Channel Mask is missing"); + checkState(mPskc != null, "PSKc is missing"); + checkState(mNetworkKey != null, "Network Key is missing"); + checkState(mMeshLocalPrefix != null, "Mesh Local Prefix is missing"); + checkState(mSecurityPolicy != null, "Security Policy is missing"); + + int length = getTotalDatasetLength(); + if (length > LENGTH_MAX_DATASET_TLVS) { + throw new IllegalStateException( + String.format( + "Total dataset length exceeds max length %d (actual is %d)", + LENGTH_MAX_DATASET_TLVS, length)); + } + + return new ActiveOperationalDataset(this); + } + + private int getTotalDatasetLength() { + int length = + 2 * 9 // 9 fields with 1 byte of type and 1 byte of length + + OperationalDatasetTimestamp.LENGTH_TIMESTAMP + + mNetworkName.getBytes(UTF_8).length + + LENGTH_EXTENDED_PAN_ID + + LENGTH_PAN_ID + + LENGTH_CHANNEL + + LENGTH_PSKC + + LENGTH_NETWORK_KEY + + LENGTH_MESH_LOCAL_PREFIX_BITS / 8 + + mSecurityPolicy.toTlvValue().length; + + for (int i = 0; i < mChannelMask.size(); i++) { + length += 2 + mChannelMask.valueAt(i).length; + } + + // For the type and length bytes of the Channel Mask TLV because the masks are encoded + // as TLVs in TLV. + length += 2; + + for (int i = 0; i < mUnknownTlvs.size(); i++) { + length += 2 + mUnknownTlvs.valueAt(i).length; + } + + return length; + } + } + + /** + * The Security Policy of Thread Operational Dataset which provides an administrator with a way + * to enable or disable certain security related behaviors. + */ + public static final class SecurityPolicy { + /** The default Rotation Time in hours. */ + public static final int DEFAULT_ROTATION_TIME_HOURS = 672; + /** The minimum length of Security Policy flags in bytes. */ + public static final int LENGTH_MIN_SECURITY_POLICY_FLAGS = 1; + /** The length of Rotation Time TLV value in bytes. */ + private static final int LENGTH_SECURITY_POLICY_ROTATION_TIME = 2; + + private final int mRotationTimeHours; + private final byte[] mFlags; + + /** + * Creates a new {@link SecurityPolicy} object. + * + * @param rotationTimeHours the value for Thread key rotation in hours. Must be in range of + * 0x1-0xffff. + * @param flags security policy flags with length of either 1 byte for Thread 1.1 or 2 bytes + * for Thread 1.2 or higher. + * @throws IllegalArgumentException if {@code rotationTimeHours} is not in range of + * 0x1-0xffff or length of {@code flags} is smaller than {@link + * #LENGTH_MIN_SECURITY_POLICY_FLAGS}. + */ + public SecurityPolicy( + @IntRange(from = 0x1, to = 0xffff) int rotationTimeHours, + @NonNull @Size(min = LENGTH_MIN_SECURITY_POLICY_FLAGS) byte[] flags) { + requireNonNull(flags, "flags cannot be null"); + checkArgument( + rotationTimeHours >= 1 && rotationTimeHours <= 0xffff, + "Rotation time exceeds allowed range (rotationTimeHours = %d, allowedRange =" + + " [0x1, 0xffff])", + rotationTimeHours); + checkArgument( + flags.length >= LENGTH_MIN_SECURITY_POLICY_FLAGS, + "Invalid security policy flags length (length = %d, minimumLength = %d)", + flags.length, + LENGTH_MIN_SECURITY_POLICY_FLAGS); + this.mRotationTimeHours = rotationTimeHours; + this.mFlags = flags.clone(); + } + + /** + * Creates a new {@link SecurityPolicy} object from the Security Policy TLV value. + * + * @hide + */ + @VisibleForTesting + @NonNull + public static SecurityPolicy fromTlvValue(byte[] encodedSecurityPolicy) { + checkArgument( + encodedSecurityPolicy.length + >= LENGTH_SECURITY_POLICY_ROTATION_TIME + + LENGTH_MIN_SECURITY_POLICY_FLAGS, + "Invalid Security Policy TLV length (length = %d, minimumLength = %d)", + encodedSecurityPolicy.length, + LENGTH_SECURITY_POLICY_ROTATION_TIME + LENGTH_MIN_SECURITY_POLICY_FLAGS); + + return new SecurityPolicy( + ((encodedSecurityPolicy[0] & 0xff) << 8) | (encodedSecurityPolicy[1] & 0xff), + Arrays.copyOfRange( + encodedSecurityPolicy, + LENGTH_SECURITY_POLICY_ROTATION_TIME, + encodedSecurityPolicy.length)); + } + + /** + * Converts this {@link SecurityPolicy} object to Security Policy TLV value. + * + * @hide + */ + @VisibleForTesting + @NonNull + public byte[] toTlvValue() { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + result.write(mRotationTimeHours >> 8); + result.write(mRotationTimeHours); + result.write(mFlags, 0, mFlags.length); + return result.toByteArray(); + } + + /** Returns the Security Policy Rotation Time in hours. */ + @IntRange(from = 0x1, to = 0xffff) + public int getRotationTimeHours() { + return mRotationTimeHours; + } + + /** Returns 1 byte flags for Thread 1.1 or 2 bytes flags for Thread 1.2. */ + @NonNull + @Size(min = LENGTH_MIN_SECURITY_POLICY_FLAGS) + public byte[] getFlags() { + return mFlags.clone(); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } else if (!(other instanceof SecurityPolicy)) { + return false; + } else { + SecurityPolicy otherSecurityPolicy = (SecurityPolicy) other; + return mRotationTimeHours == otherSecurityPolicy.mRotationTimeHours + && Arrays.equals(mFlags, otherSecurityPolicy.mFlags); + } + } + + @Override + public int hashCode() { + return deepHashCode(mRotationTimeHours, mFlags); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{rotation=") + .append(mRotationTimeHours) + .append(", flags=") + .append(dumpHexString(mFlags)) + .append("}"); + return sb.toString(); + } + } +} diff --git a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java new file mode 100644 index 0000000000..bda9373d08 --- /dev/null +++ b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.thread; + +import static com.android.internal.util.Preconditions.checkArgument; + +import static java.util.Objects.requireNonNull; + +import android.annotation.FlaggedApi; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; + +import java.nio.ByteBuffer; +import java.time.Instant; +import java.util.Objects; + +/** + * The timestamp of Thread Operational Dataset. + * + * @see ActiveOperationalDataset + * @see PendingOperationalDataset + * @hide + */ +@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED) +@SystemApi +public final class OperationalDatasetTimestamp { + /** @hide */ + public static final int LENGTH_TIMESTAMP = Long.BYTES; + + private static final long TICKS_UPPER_BOUND = 0x8000; + + private final Instant mInstant; + private final boolean mIsAuthoritativeSource; + + /** + * Creates a new {@link OperationalDatasetTimestamp} object from an {@link Instant}. + * + *

The {@code seconds} is set to {@code instant.getEpochSecond()}, {@code ticks} is set to + * {@link instant#getNano()} based on frequency of 32768 Hz, and {@code isAuthoritativeSource} + * is set to {@code true}. + * + * @throws IllegalArgumentException if {@code instant.getEpochSecond()} is larger than {@code + * 0xffffffffffffL} + */ + @NonNull + public static OperationalDatasetTimestamp fromInstant(@NonNull Instant instant) { + return new OperationalDatasetTimestamp(instant, /* isAuthoritativeSource= */ true); + } + + /** Converts this {@link OperationalDatasetTimestamp} object to an {@link Instant}. */ + @NonNull + public Instant toInstant() { + return mInstant; + } + + /** + * Creates a new {@link OperationalDatasetTimestamp} object from the OperationalDatasetTimestamp + * TLV value. + * + * @hide + */ + @NonNull + public static OperationalDatasetTimestamp fromTlvValue(@NonNull byte[] encodedTimestamp) { + requireNonNull(encodedTimestamp, "encodedTimestamp cannot be null"); + checkArgument( + encodedTimestamp.length == LENGTH_TIMESTAMP, + "Invalid Thread OperationalDatasetTimestamp length (length = %d," + + " expectedLength=%d)", + encodedTimestamp.length, + LENGTH_TIMESTAMP); + long longTimestamp = ByteBuffer.wrap(encodedTimestamp).getLong(); + return new OperationalDatasetTimestamp( + (longTimestamp >> 16) & 0x0000ffffffffffffL, + (int) ((longTimestamp >> 1) & 0x7fffL), + (longTimestamp & 0x01) != 0); + } + + /** + * Converts this {@link OperationalDatasetTimestamp} object to Thread TLV value. + * + * @hide + */ + @NonNull + public byte[] toTlvValue() { + byte[] tlv = new byte[LENGTH_TIMESTAMP]; + ByteBuffer buffer = ByteBuffer.wrap(tlv); + long encodedValue = + (mInstant.getEpochSecond() << 16) + | ((mInstant.getNano() * TICKS_UPPER_BOUND / 1000000000L) << 1) + | (mIsAuthoritativeSource ? 1 : 0); + buffer.putLong(encodedValue); + return tlv; + } + + /** + * Creates a new {@link OperationalDatasetTimestamp} object. + * + * @param seconds the value encodes a Unix Time value. Must be in the range of + * 0x0-0xffffffffffffL + * @param ticks the value encodes the fractional Unix Time value in 32.768 kHz resolution. Must + * be in the range of 0x0-0x7fff + * @param isAuthoritativeSource the flag indicates the time was obtained from an authoritative + * source: either NTP (Network Time Protocol), GPS (Global Positioning System), cell + * network, or other method + * @throws IllegalArgumentException if the {@code seconds} is not in range of + * 0x0-0xffffffffffffL or {@code ticks} is not in range of 0x0-0x7fff + */ + public OperationalDatasetTimestamp( + @IntRange(from = 0x0, to = 0xffffffffffffL) long seconds, + @IntRange(from = 0x0, to = 0x7fff) int ticks, + boolean isAuthoritativeSource) { + this(makeInstant(seconds, ticks), isAuthoritativeSource); + } + + private static Instant makeInstant(long seconds, int ticks) { + checkArgument( + seconds >= 0 && seconds <= 0xffffffffffffL, + "seconds exceeds allowed range (seconds = %d," + + " allowedRange = [0x0, 0xffffffffffffL])", + seconds); + checkArgument( + ticks >= 0 && ticks <= 0x7fff, + "ticks exceeds allowed ranged (ticks = %d, allowedRange" + " = [0x0, 0x7fff])", + ticks); + long nanos = Math.round((double) ticks * 1000000000L / TICKS_UPPER_BOUND); + return Instant.ofEpochSecond(seconds, nanos); + } + + /** + * Creates new {@link OperationalDatasetTimestamp} object. + * + * @throws IllegalArgumentException if {@code instant.getEpochSecond()} is larger than {@code + * 0xffffffffffffL} + */ + private OperationalDatasetTimestamp(@NonNull Instant instant, boolean isAuthoritativeSource) { + requireNonNull(instant, "instant cannot be null"); + long seconds = instant.getEpochSecond(); + checkArgument( + seconds >= 0 && seconds <= 0xffffffffffffL, + "instant seconds exceeds allowed range (seconds = %d, allowedRange = [0x0," + + " 0xffffffffffffL])", + seconds); + mInstant = instant; + mIsAuthoritativeSource = isAuthoritativeSource; + } + + /** + * Returns the rounded ticks converted from the nano seconds. + * + *

Note that rhe return value can be as large as {@code TICKS_UPPER_BOUND}. + */ + private static int getRoundedTicks(long nanos) { + return (int) Math.round((double) nanos * TICKS_UPPER_BOUND / 1000000000L); + } + + /** Returns the seconds portion of the timestamp. */ + public @IntRange(from = 0x0, to = 0xffffffffffffL) long getSeconds() { + return mInstant.getEpochSecond() + getRoundedTicks(mInstant.getNano()) / TICKS_UPPER_BOUND; + } + + /** Returns the ticks portion of the timestamp. */ + public @IntRange(from = 0x0, to = 0x7fff) int getTicks() { + // the rounded ticks can be 0x8000 if mInstant.getNano() >= 999984742 + return (int) (getRoundedTicks(mInstant.getNano()) % TICKS_UPPER_BOUND); + } + + /** Returns {@code true} if the timestamp comes from an authoritative source. */ + public boolean isAuthoritativeSource() { + return mIsAuthoritativeSource; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{seconds=") + .append(getSeconds()) + .append(", ticks=") + .append(getTicks()) + .append(", isAuthoritativeSource=") + .append(isAuthoritativeSource()) + .append(", instant=") + .append(toInstant()) + .append("}"); + return sb.toString(); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } else if (!(other instanceof OperationalDatasetTimestamp)) { + return false; + } else { + OperationalDatasetTimestamp otherTimestamp = (OperationalDatasetTimestamp) other; + return mInstant.equals(otherTimestamp.mInstant) + && mIsAuthoritativeSource == otherTimestamp.mIsAuthoritativeSource; + } + } + + @Override + public int hashCode() { + return Objects.hash(mInstant, mIsAuthoritativeSource); + } +} diff --git a/thread/framework/java/android/net/thread/PendingOperationalDataset.aidl b/thread/framework/java/android/net/thread/PendingOperationalDataset.aidl new file mode 100644 index 0000000000..e5bc05e288 --- /dev/null +++ b/thread/framework/java/android/net/thread/PendingOperationalDataset.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.thread; + +parcelable PendingOperationalDataset; diff --git a/thread/framework/java/android/net/thread/PendingOperationalDataset.java b/thread/framework/java/android/net/thread/PendingOperationalDataset.java new file mode 100644 index 0000000000..4762d7f54b --- /dev/null +++ b/thread/framework/java/android/net/thread/PendingOperationalDataset.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.thread; + +import static com.android.internal.util.Preconditions.checkArgument; + +import static java.util.Objects.requireNonNull; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.SparseArray; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Objects; + +/** + * Data interface for managing a Thread Pending Operational Dataset. + * + *

The Pending Operational Dataset represents an Operational Dataset which will become Active in + * a given delay. This is typically used to deploy new network parameters (e.g. Network Key or + * Channel) to all devices in the network. + * + * @hide + */ +@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED) +@SystemApi +public final class PendingOperationalDataset implements Parcelable { + // Value defined in Thread spec 8.10.1.16 + private static final int TYPE_PENDING_TIMESTAMP = 51; + + // Values defined in Thread spec 8.10.1.17 + private static final int TYPE_DELAY_TIMER = 52; + private static final int LENGTH_DELAY_TIMER_BYTES = 4; + + @NonNull + public static final Creator CREATOR = + new Creator<>() { + @Override + public PendingOperationalDataset createFromParcel(Parcel in) { + return PendingOperationalDataset.fromThreadTlvs(in.createByteArray()); + } + + @Override + public PendingOperationalDataset[] newArray(int size) { + return new PendingOperationalDataset[size]; + } + }; + + @NonNull private final ActiveOperationalDataset mActiveOpDataset; + @NonNull private final OperationalDatasetTimestamp mPendingTimestamp; + @NonNull private final Duration mDelayTimer; + + /** Creates a new {@link PendingOperationalDataset} object. */ + public PendingOperationalDataset( + @NonNull ActiveOperationalDataset activeOpDataset, + @NonNull OperationalDatasetTimestamp pendingTimestamp, + @NonNull Duration delayTimer) { + requireNonNull(activeOpDataset, "activeOpDataset cannot be null"); + requireNonNull(pendingTimestamp, "pendingTimestamp cannot be null"); + requireNonNull(delayTimer, "delayTimer cannot be null"); + this.mActiveOpDataset = activeOpDataset; + this.mPendingTimestamp = pendingTimestamp; + this.mDelayTimer = delayTimer; + } + + /** + * Creates a new {@link PendingOperationalDataset} object from a series of Thread TLVs. + * + *

{@code tlvs} can be obtained from the value of a Thread Pending Operational Dataset TLV + * (see the Thread + * specification for the definition) or the return value of {@link #toThreadTlvs}. + * + * @throws IllegalArgumentException if {@code tlvs} is malformed or contains an invalid Thread + * TLV + */ + @NonNull + public static PendingOperationalDataset fromThreadTlvs(@NonNull byte[] tlvs) { + requireNonNull(tlvs, "tlvs cannot be null"); + + SparseArray newUnknownTlvs = new SparseArray<>(); + OperationalDatasetTimestamp pendingTimestamp = null; + Duration delayTimer = null; + ActiveOperationalDataset activeDataset = ActiveOperationalDataset.fromThreadTlvs(tlvs); + SparseArray unknownTlvs = activeDataset.getUnknownTlvs(); + for (int i = 0; i < unknownTlvs.size(); i++) { + int key = unknownTlvs.keyAt(i); + byte[] value = unknownTlvs.valueAt(i); + switch (key) { + case TYPE_PENDING_TIMESTAMP: + pendingTimestamp = OperationalDatasetTimestamp.fromTlvValue(value); + break; + case TYPE_DELAY_TIMER: + checkArgument( + value.length == LENGTH_DELAY_TIMER_BYTES, + "Invalid delay timer (length = %d, expectedLength = %d)", + value.length, + LENGTH_DELAY_TIMER_BYTES); + int millis = ByteBuffer.wrap(value).getInt(); + delayTimer = Duration.ofMillis(Integer.toUnsignedLong(millis)); + break; + default: + newUnknownTlvs.put(key, value); + break; + } + } + + if (pendingTimestamp == null) { + throw new IllegalArgumentException("Pending Timestamp is missing"); + } + if (delayTimer == null) { + throw new IllegalArgumentException("Delay Timer is missing"); + } + + activeDataset = + new ActiveOperationalDataset.Builder(activeDataset) + .setUnknownTlvs(newUnknownTlvs) + .build(); + return new PendingOperationalDataset(activeDataset, pendingTimestamp, delayTimer); + } + + /** Returns the Active Operational Dataset. */ + @NonNull + public ActiveOperationalDataset getActiveOperationalDataset() { + return mActiveOpDataset; + } + + /** Returns the Pending Timestamp. */ + @NonNull + public OperationalDatasetTimestamp getPendingTimestamp() { + return mPendingTimestamp; + } + + /** Returns the Delay Timer. */ + @NonNull + public Duration getDelayTimer() { + return mDelayTimer; + } + + /** + * Converts this {@link PendingOperationalDataset} object to a series of Thread TLVs. + * + *

See the Thread + * specification for the definition of the Thread TLV format. + */ + @NonNull + public byte[] toThreadTlvs() { + ByteArrayOutputStream dataset = new ByteArrayOutputStream(); + + byte[] activeDatasetBytes = mActiveOpDataset.toThreadTlvs(); + dataset.write(activeDatasetBytes, 0, activeDatasetBytes.length); + + dataset.write(TYPE_PENDING_TIMESTAMP); + byte[] pendingTimestampBytes = mPendingTimestamp.toTlvValue(); + dataset.write(pendingTimestampBytes.length); + dataset.write(pendingTimestampBytes, 0, pendingTimestampBytes.length); + + dataset.write(TYPE_DELAY_TIMER); + byte[] delayTimerBytes = new byte[LENGTH_DELAY_TIMER_BYTES]; + ByteBuffer.wrap(delayTimerBytes).putInt((int) mDelayTimer.toMillis()); + dataset.write(delayTimerBytes.length); + dataset.write(delayTimerBytes, 0, delayTimerBytes.length); + + return dataset.toByteArray(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (!(other instanceof PendingOperationalDataset)) { + return false; + } else { + PendingOperationalDataset otherDataset = (PendingOperationalDataset) other; + return mActiveOpDataset.equals(otherDataset.mActiveOpDataset) + && mPendingTimestamp.equals(otherDataset.mPendingTimestamp) + && mDelayTimer.equals(otherDataset.mDelayTimer); + } + } + + @Override + public int hashCode() { + return Objects.hash(mActiveOpDataset, mPendingTimestamp, mDelayTimer); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{activeDataset=") + .append(getActiveOperationalDataset()) + .append(", pendingTimestamp=") + .append(getPendingTimestamp()) + .append(", delayTimer=") + .append(getDelayTimer()) + .append("}"); + return sb.toString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeByteArray(toThreadTlvs()); + } +} diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp index 278798edb4..ce770e0875 100644 --- a/thread/tests/cts/Android.bp +++ b/thread/tests/cts/Android.bp @@ -37,6 +37,7 @@ android_test { "androidx.test.ext.junit", "compatibility-device-util-axt", "ctstestrunner-axt", + "guava-android-testlib", "net-tests-utils", "truth", ], diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml index 5ba605f573..ffc181c58d 100644 --- a/thread/tests/cts/AndroidTest.xml +++ b/thread/tests/cts/AndroidTest.xml @@ -47,5 +47,7 @@ diff --git a/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java b/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java new file mode 100644 index 0000000000..39df21b340 --- /dev/null +++ b/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java @@ -0,0 +1,737 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.thread.cts; + +import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ; + +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; + +import static com.google.common.io.BaseEncoding.base16; +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.net.IpPrefix; +import android.net.thread.ActiveOperationalDataset; +import android.net.thread.ActiveOperationalDataset.Builder; +import android.net.thread.ActiveOperationalDataset.SecurityPolicy; +import android.net.thread.OperationalDatasetTimestamp; +import android.util.SparseArray; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.google.common.primitives.Bytes; +import com.google.common.testing.EqualsTester; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.ByteArrayOutputStream; +import java.util.Arrays; + +/** CTS tests for {@link ActiveOperationalDataset}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class ActiveOperationalDatasetTest { + private static final int TYPE_ACTIVE_TIMESTAMP = 14; + private static final int TYPE_CHANNEL = 0; + private static final int TYPE_CHANNEL_MASK = 53; + private static final int TYPE_EXTENDED_PAN_ID = 2; + private static final int TYPE_MESH_LOCAL_PREFIX = 7; + private static final int TYPE_NETWORK_KEY = 5; + private static final int TYPE_NETWORK_NAME = 3; + private static final int TYPE_PAN_ID = 1; + private static final int TYPE_PSKC = 4; + private static final int TYPE_SECURITY_POLICY = 12; + + // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new": + // Active Timestamp: 1 + // Channel: 19 + // Channel Mask: 0x07FFF800 + // Ext PAN ID: ACC214689BC40BDF + // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64 + // Network Key: F26B3153760F519A63BAFDDFFC80D2AF + // Network Name: OpenThread-d9a0 + // PAN ID: 0xD9A0 + // PSKc: A245479C836D551B9CA557F7B9D351B4 + // Security Policy: 672 onrcb + private static final byte[] VALID_DATASET = + base16().decode( + "0E080000000000010000000300001335060004001FFFE002" + + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31" + + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561" + + "642D643961300102D9A00410A245479C836D551B9CA557F7" + + "B9D351B40C0402A0FFF8"); + + private static byte[] removeTlv(byte[] dataset, int type) { + ByteArrayOutputStream os = new ByteArrayOutputStream(dataset.length); + int i = 0; + while (i < dataset.length) { + int ty = dataset[i++] & 0xff; + byte length = dataset[i++]; + if (ty != type) { + byte[] value = Arrays.copyOfRange(dataset, i, i + length); + os.write(ty); + os.write(length); + os.writeBytes(value); + } + i += length; + } + return os.toByteArray(); + } + + private static byte[] addTlv(byte[] dataset, String tlvHex) { + return Bytes.concat(dataset, base16().decode(tlvHex)); + } + + private static byte[] replaceTlv(byte[] dataset, int type, String newTlvHex) { + return addTlv(removeTlv(dataset, type), newTlvHex); + } + + @Test + public void parcelable_parcelingIsLossLess() { + ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET); + + assertParcelingIsLossless(dataset); + } + + @Test + public void fromThreadTlvs_tooLongTlv_throwsIllegalArgument() { + byte[] invalidTlv = new byte[255]; + invalidTlv[0] = (byte) 0xff; + + // This is invalid because the TLV has max total length of 254 bytes and the value length + // can't exceeds 252 ( = 254 - 1 - 1) + invalidTlv[1] = (byte) 253; + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_invalidNetworkKeyTlv_throwsIllegalArgument() { + byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_NETWORK_KEY, "05080000000000000000"); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_noNetworkKeyTlv_throwsIllegalArgument() { + byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_NETWORK_KEY); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_invalidActiveTimestampTlv_throwsIllegalArgument() { + byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_ACTIVE_TIMESTAMP, "0E0700000000010000"); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_noActiveTimestampTlv_throwsIllegalArgument() { + byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_ACTIVE_TIMESTAMP); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_invalidNetworkNameTlv_emptyName_throwsIllegalArgument() { + byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_NETWORK_NAME, "0300"); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_invalidNetworkNameTlv_tooLongName_throwsIllegalArgument() { + byte[] invalidTlv = + replaceTlv( + VALID_DATASET, TYPE_NETWORK_NAME, "03114142434445464748494A4B4C4D4E4F5051"); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_noNetworkNameTlv_throwsIllegalArgument() { + byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_NETWORK_NAME); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_invalidChannelTlv_channelMissing_throwsIllegalArgument() { + byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000100"); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_undefinedChannelPage_success() { + byte[] datasetTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "0003010020"); + + ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlv); + + assertThat(dataset.getChannelPage()).isEqualTo(0x01); + assertThat(dataset.getChannel()).isEqualTo(0x20); + } + + @Test + public void fromThreadTlvs_invalid2P4GhzChannel_throwsIllegalArgument() { + byte[] invalidTlv1 = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000300000A"); + byte[] invalidTlv2 = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000300001B"); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv1)); + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv2)); + } + + @Test + public void fromThreadTlvs_valid2P4GhzChannelTlv_success() { + byte[] validTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "0003000010"); + + ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(validTlv); + + assertThat(dataset.getChannel()).isEqualTo(16); + } + + @Test + public void fromThreadTlvs_noChannelTlv_throwsIllegalArgument() { + byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_CHANNEL); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_prematureEndOfChannelMaskEntry_throwsIllegalArgument() { + byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "350100"); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_inconsistentChannelMaskLength_throwsIllegalArgument() { + byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "3506000500010000"); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_unsupportedChannelMaskLength_success() { + ActiveOperationalDataset dataset = + ActiveOperationalDataset.fromThreadTlvs( + replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "350700050001000000")); + + SparseArray channelMask = dataset.getChannelMask(); + assertThat(channelMask.size()).isEqualTo(1); + assertThat(channelMask.get(CHANNEL_PAGE_24_GHZ)) + .isEqualTo(new byte[] {0x00, 0x01, 0x00, 0x00, 0x00}); + } + + @Test + public void fromThreadTlvs_noChannelMaskTlv_throwsIllegalArgument() { + byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_CHANNEL_MASK); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_invalidPanIdTlv_throwsIllegalArgument() { + byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_PAN_ID, "010101"); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_noPanIdTlv_throwsIllegalArgument() { + byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_PAN_ID); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_invalidExtendedPanIdTlv_throwsIllegalArgument() { + byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_EXTENDED_PAN_ID, "020700010203040506"); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_noExtendedPanIdTlv_throwsIllegalArgument() { + byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_EXTENDED_PAN_ID); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_invalidPskcTlv_throwsIllegalArgument() { + byte[] invalidTlv = + replaceTlv(VALID_DATASET, TYPE_PSKC, "0411000102030405060708090A0B0C0D0E0F10"); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_noPskcTlv_throwsIllegalArgument() { + byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_PSKC); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_invalidMeshLocalPrefixTlv_throwsIllegalArgument() { + byte[] invalidTlv = + replaceTlv(VALID_DATASET, TYPE_MESH_LOCAL_PREFIX, "0709FD0001020304050607"); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_noMeshLocalPrefixTlv_throwsIllegalArgument() { + byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_MESH_LOCAL_PREFIX); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_tooShortSecurityPolicyTlv_throwsIllegalArgument() { + byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_SECURITY_POLICY, "0C0101"); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_noSecurityPolicyTlv_throwsIllegalArgument() { + byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_SECURITY_POLICY); + + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv)); + } + + @Test + public void fromThreadTlvs_lengthAndDataMissing_throwsIllegalArgument() { + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(new byte[] {(byte) 0x00})); + } + + @Test + public void fromThreadTlvs_prematureEndOfData_throwsIllegalArgument() { + assertThrows( + IllegalArgumentException.class, + () -> ActiveOperationalDataset.fromThreadTlvs(new byte[] {0x00, 0x03, 0x00, 0x00})); + } + + @Test + public void fromThreadTlvs_validFullDataset_success() { + // A valid Thread active operational dataset: + // Active Timestamp: 1 + // Channel: 19 + // Channel Mask: 0x07FFF800 + // Ext PAN ID: ACC214689BC40BDF + // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64 + // Network Key: F26B3153760F519A63BAFDDFFC80D2AF + // Network Name: OpenThread-d9a0 + // PAN ID: 0xD9A0 + // PSKc: A245479C836D551B9CA557F7B9D351B4 + // Security Policy: 672 onrcb + byte[] validDatasetTlv = + base16().decode( + "0E080000000000010000000300001335060004001FFFE002" + + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31" + + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561" + + "642D643961300102D9A00410A245479C836D551B9CA557F7" + + "B9D351B40C0402A0FFF8"); + + ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(validDatasetTlv); + + assertThat(dataset.getNetworkKey()) + .isEqualTo(base16().decode("F26B3153760F519A63BAFDDFFC80D2AF")); + assertThat(dataset.getPanId()).isEqualTo(0xd9a0); + assertThat(dataset.getExtendedPanId()).isEqualTo(base16().decode("ACC214689BC40BDF")); + assertThat(dataset.getChannel()).isEqualTo(19); + assertThat(dataset.getNetworkName()).isEqualTo("OpenThread-d9a0"); + assertThat(dataset.getPskc()) + .isEqualTo(base16().decode("A245479C836D551B9CA557F7B9D351B4")); + assertThat(dataset.getActiveTimestamp()) + .isEqualTo(new OperationalDatasetTimestamp(1, 0, false)); + SparseArray channelMask = dataset.getChannelMask(); + assertThat(channelMask.size()).isEqualTo(1); + assertThat(channelMask.get(CHANNEL_PAGE_24_GHZ)) + .isEqualTo(new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0}); + assertThat(dataset.getMeshLocalPrefix()) + .isEqualTo(new IpPrefix("fd64:db12:25f4:7e0b::/64")); + assertThat(dataset.getSecurityPolicy()) + .isEqualTo(new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8})); + } + + @Test + public void fromThreadTlvs_containsUnknownTlvs_unknownTlvsRetained() { + final byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET, "AA01FFBB020102"); + + ActiveOperationalDataset dataset = + ActiveOperationalDataset.fromThreadTlvs(datasetWithUnknownTlvs); + + byte[] newDatasetTlvs = dataset.toThreadTlvs(); + String newDatasetTlvsHex = base16().encode(newDatasetTlvs); + assertThat(newDatasetTlvs.length).isEqualTo(datasetWithUnknownTlvs.length); + assertThat(newDatasetTlvsHex).contains("AA01FF"); + assertThat(newDatasetTlvsHex).contains("BB020102"); + } + + @Test + public void toThreadTlvs_conversionIsLossLess() { + ActiveOperationalDataset dataset1 = ActiveOperationalDataset.createRandomDataset(); + + ActiveOperationalDataset dataset2 = + ActiveOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs()); + + assertThat(dataset2).isEqualTo(dataset1); + } + + @Test + public void builder_buildWithdefaultValues_throwsIllegalState() { + assertThrows(IllegalStateException.class, () -> new Builder().build()); + } + + @Test + public void builder_setValidNetworkKey_success() { + final byte[] networkKey = + new byte[] { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f + }; + + ActiveOperationalDataset dataset = + new Builder(ActiveOperationalDataset.createRandomDataset()) + .setNetworkKey(networkKey) + .build(); + + assertThat(dataset.getNetworkKey()).isEqualTo(networkKey); + } + + @Test + public void builder_setInvalidNetworkKey_throwsIllegalArgument() { + byte[] invalidNetworkKey = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}; + Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset()); + + assertThrows( + IllegalArgumentException.class, () -> builder.setNetworkKey(invalidNetworkKey)); + } + + @Test + public void builder_setValidExtendedPanId_success() { + byte[] extendedPanId = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}; + + ActiveOperationalDataset dataset = + new Builder(ActiveOperationalDataset.createRandomDataset()) + .setExtendedPanId(extendedPanId) + .build(); + + assertThat(dataset.getExtendedPanId()).isEqualTo(extendedPanId); + } + + @Test + public void builder_setInvalidExtendedPanId_throwsIllegalArgument() { + byte[] extendedPanId = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; + Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset()); + + assertThrows(IllegalArgumentException.class, () -> builder.setExtendedPanId(extendedPanId)); + } + + @Test + public void builder_setValidPanId_success() { + ActiveOperationalDataset dataset = + new Builder(ActiveOperationalDataset.createRandomDataset()) + .setPanId(0xfffe) + .build(); + + assertThat(dataset.getPanId()).isEqualTo(0xfffe); + } + + @Test + public void builder_setInvalidPanId_throwsIllegalArgument() { + Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset()); + + assertThrows(IllegalArgumentException.class, () -> builder.setPanId(0xffff)); + } + + @Test + public void builder_setInvalidChannel_throwsIllegalArgument() { + Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset()); + + assertThrows(IllegalArgumentException.class, () -> builder.setChannel(0, 0)); + assertThrows(IllegalArgumentException.class, () -> builder.setChannel(0, 27)); + } + + @Test + public void builder_setValid2P4GhzChannel_success() { + ActiveOperationalDataset dataset = + new Builder(ActiveOperationalDataset.createRandomDataset()) + .setChannel(CHANNEL_PAGE_24_GHZ, 16) + .build(); + + assertThat(dataset.getChannel()).isEqualTo(16); + assertThat(dataset.getChannelPage()).isEqualTo(CHANNEL_PAGE_24_GHZ); + } + + @Test + public void builder_setValidNetworkName_success() { + ActiveOperationalDataset dataset = + new Builder(ActiveOperationalDataset.createRandomDataset()) + .setNetworkName("ot-network") + .build(); + + assertThat(dataset.getNetworkName()).isEqualTo("ot-network"); + } + + @Test + public void builder_setEmptyNetworkName_throwsIllegalArgument() { + Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset()); + + assertThrows(IllegalArgumentException.class, () -> builder.setNetworkName("")); + } + + @Test + public void builder_setTooLongNetworkName_throwsIllegalArgument() { + Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset()); + + assertThrows( + IllegalArgumentException.class, () -> builder.setNetworkName("openthread-network")); + } + + @Test + public void builder_setTooLongUtf8NetworkName_throwsIllegalArgument() { + Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset()); + + // UTF-8 encoded length of "我的线程网络" is 18 bytes which exceeds the max length + assertThrows(IllegalArgumentException.class, () -> builder.setNetworkName("我的线程网络")); + } + + @Test + public void builder_setValidUtf8NetworkName_success() { + ActiveOperationalDataset dataset = + new Builder(ActiveOperationalDataset.createRandomDataset()) + .setNetworkName("我的网络") + .build(); + + assertThat(dataset.getNetworkName()).isEqualTo("我的网络"); + } + + @Test + public void builder_setValidPskc_success() { + byte[] pskc = base16().decode("A245479C836D551B9CA557F7B9D351B4"); + + ActiveOperationalDataset dataset = + new Builder(ActiveOperationalDataset.createRandomDataset()).setPskc(pskc).build(); + + assertThat(dataset.getPskc()).isEqualTo(pskc); + } + + @Test + public void builder_setTooLongPskc_throwsIllegalArgument() { + byte[] tooLongPskc = base16().decode("A245479C836D551B9CA557F7B9D351B400"); + Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset()); + + assertThrows(IllegalArgumentException.class, () -> builder.setPskc(tooLongPskc)); + } + + @Test + public void builder_setValidChannelMask_success() { + Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset()); + SparseArray channelMask = new SparseArray(1); + channelMask.put(0, new byte[] {0x00, 0x00, 0x01, 0x00}); + + ActiveOperationalDataset dataset = builder.setChannelMask(channelMask).build(); + + SparseArray resultChannelMask = dataset.getChannelMask(); + assertThat(resultChannelMask.size()).isEqualTo(1); + assertThat(resultChannelMask.get(0)).isEqualTo(new byte[] {0x00, 0x00, 0x01, 0x00}); + } + + @Test + public void builder_setEmptyChannelMask_throwsIllegalArgument() { + Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset()); + + assertThrows( + IllegalArgumentException.class, + () -> builder.setChannelMask(new SparseArray())); + } + + @Test + public void builder_setValidActiveTimestamp_success() { + ActiveOperationalDataset dataset = + new Builder(ActiveOperationalDataset.createRandomDataset()) + .setActiveTimestamp( + new OperationalDatasetTimestamp( + /* seconds= */ 1, + /* ticks= */ 0, + /* isAuthoritativeSource= */ true)) + .build(); + + assertThat(dataset.getActiveTimestamp().getSeconds()).isEqualTo(1); + assertThat(dataset.getActiveTimestamp().getTicks()).isEqualTo(0); + assertThat(dataset.getActiveTimestamp().isAuthoritativeSource()).isTrue(); + } + + @Test + public void builder_wrongMeshLocalPrefixLength_throwsIllegalArguments() { + Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset()); + + // The Mesh-Local Prefix length must be 64 bits + assertThrows( + IllegalArgumentException.class, + () -> builder.setMeshLocalPrefix(new IpPrefix("fd00::/32"))); + assertThrows( + IllegalArgumentException.class, + () -> builder.setMeshLocalPrefix(new IpPrefix("fd00::/96"))); + + // The Mesh-Local Prefix must start with 0xfd + assertThrows( + IllegalArgumentException.class, + () -> builder.setMeshLocalPrefix(new IpPrefix("fc00::/64"))); + } + + @Test + public void builder_meshLocalPrefixNotStartWith0xfd_throwsIllegalArguments() { + Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset()); + + assertThrows( + IllegalArgumentException.class, + () -> builder.setMeshLocalPrefix(new IpPrefix("fc00::/64"))); + } + + @Test + public void builder_setValidMeshLocalPrefix_success() { + ActiveOperationalDataset dataset = + new Builder(ActiveOperationalDataset.createRandomDataset()) + .setMeshLocalPrefix(new IpPrefix("fd00::/64")) + .build(); + + assertThat(dataset.getMeshLocalPrefix()).isEqualTo(new IpPrefix("fd00::/64")); + } + + @Test + public void builder_setValid1P2SecurityPolicy_success() { + ActiveOperationalDataset dataset = + new Builder(ActiveOperationalDataset.createRandomDataset()) + .setSecurityPolicy( + new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8})) + .build(); + + assertThat(dataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672); + assertThat(dataset.getSecurityPolicy().getFlags()) + .isEqualTo(new byte[] {(byte) 0xff, (byte) 0xf8}); + } + + @Test + public void builder_setValid1P1SecurityPolicy_success() { + ActiveOperationalDataset dataset = + new Builder(ActiveOperationalDataset.createRandomDataset()) + .setSecurityPolicy(new SecurityPolicy(672, new byte[] {(byte) 0xff})) + .build(); + + assertThat(dataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672); + assertThat(dataset.getSecurityPolicy().getFlags()).isEqualTo(new byte[] {(byte) 0xff}); + } + + @Test + public void securityPolicy_invalidRotationTime_throwsIllegalArguments() { + assertThrows( + IllegalArgumentException.class, + () -> new SecurityPolicy(0, new byte[] {(byte) 0xff, (byte) 0xf8})); + assertThrows( + IllegalArgumentException.class, + () -> new SecurityPolicy(0x1ffff, new byte[] {(byte) 0xff, (byte) 0xf8})); + } + + @Test + public void securityPolicy_emptyFlags_throwsIllegalArguments() { + assertThrows(IllegalArgumentException.class, () -> new SecurityPolicy(672, new byte[] {})); + } + + @Test + public void securityPolicy_tooLongFlags_success() { + SecurityPolicy securityPolicy = + new SecurityPolicy(672, new byte[] {0, 1, 2, 3, 4, 5, 6, 7}); + + assertThat(securityPolicy.getFlags()).isEqualTo(new byte[] {0, 1, 2, 3, 4, 5, 6, 7}); + } + + @Test + public void securityPolicy_equals() { + new EqualsTester() + .addEqualityGroup( + new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}), + new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8})) + .addEqualityGroup( + new SecurityPolicy(1, new byte[] {(byte) 0xff}), + new SecurityPolicy(1, new byte[] {(byte) 0xff})) + .addEqualityGroup( + new SecurityPolicy(1, new byte[] {(byte) 0xff, (byte) 0xf8}), + new SecurityPolicy(1, new byte[] {(byte) 0xff, (byte) 0xf8})) + .testEquals(); + } +} diff --git a/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java b/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java new file mode 100644 index 0000000000..9be3d56401 --- /dev/null +++ b/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.thread.cts; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.net.thread.OperationalDatasetTimestamp; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.google.common.testing.EqualsTester; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.time.Instant; + +/** Tests for {@link OperationalDatasetTimestamp}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class OperationalDatasetTimestampTest { + @Test + public void fromInstant_tooLargeInstant_throwsIllegalArgument() { + assertThrows( + IllegalArgumentException.class, + () -> + OperationalDatasetTimestamp.fromInstant( + Instant.ofEpochSecond(0xffffffffffffL + 1L))); + } + + @Test + public void fromInstant_ticksIsRounded() { + Instant instant = Instant.ofEpochSecond(100L); + + // 32767.5 / 32768 * 1000000000 = 999984741.2109375 and given the `ticks` is rounded, so + // the `ticks` should be 32767 for 999984741 and 0 (carried over to seconds) for 999984742. + OperationalDatasetTimestamp timestampTicks32767 = + OperationalDatasetTimestamp.fromInstant(instant.plusNanos(999984741)); + OperationalDatasetTimestamp timestampTicks0 = + OperationalDatasetTimestamp.fromInstant(instant.plusNanos(999984742)); + + assertThat(timestampTicks32767.getSeconds()).isEqualTo(100L); + assertThat(timestampTicks0.getSeconds()).isEqualTo(101L); + assertThat(timestampTicks32767.getTicks()).isEqualTo(32767); + assertThat(timestampTicks0.getTicks()).isEqualTo(0); + assertThat(timestampTicks32767.isAuthoritativeSource()).isTrue(); + assertThat(timestampTicks0.isAuthoritativeSource()).isTrue(); + } + + @Test + public void toInstant_nanosIsRounded() { + // 32767 / 32768 * 1000000000 = 999969482.421875 + assertThat(new OperationalDatasetTimestamp(100L, 32767, false).toInstant().getNano()) + .isEqualTo(999969482); + + // 32766 / 32768 * 1000000000 = 999938964.84375 + assertThat(new OperationalDatasetTimestamp(100L, 32766, false).toInstant().getNano()) + .isEqualTo(999938965); + } + + @Test + public void toInstant_onlyAuthoritativeSourceDiscarded() { + OperationalDatasetTimestamp timestamp1 = + new OperationalDatasetTimestamp(100L, 0x7fff, false); + + OperationalDatasetTimestamp timestamp2 = + OperationalDatasetTimestamp.fromInstant(timestamp1.toInstant()); + + assertThat(timestamp2.getSeconds()).isEqualTo(100L); + assertThat(timestamp2.getTicks()).isEqualTo(0x7fff); + assertThat(timestamp2.isAuthoritativeSource()).isTrue(); + } + + @Test + public void constructor_tooLargeSeconds_throwsIllegalArguments() { + assertThrows( + IllegalArgumentException.class, + () -> + new OperationalDatasetTimestamp( + /* seconds= */ 0x0001112233445566L, + /* ticks= */ 0, + /* isAuthoritativeSource= */ true)); + } + + @Test + public void constructor_tooLargeTicks_throwsIllegalArguments() { + assertThrows( + IllegalArgumentException.class, + () -> + new OperationalDatasetTimestamp( + /* seconds= */ 0x01L, + /* ticks= */ 0x8000, + /* isAuthoritativeSource= */ true)); + } + + @Test + public void equalityTests() { + new EqualsTester() + .addEqualityGroup( + new OperationalDatasetTimestamp(100, 100, false), + new OperationalDatasetTimestamp(100, 100, false)) + .addEqualityGroup( + new OperationalDatasetTimestamp(0, 0, false), + new OperationalDatasetTimestamp(0, 0, false)) + .addEqualityGroup( + new OperationalDatasetTimestamp(0xffffffffffffL, 0x7fff, true), + new OperationalDatasetTimestamp(0xffffffffffffL, 0x7fff, true)) + .testEquals(); + } +} diff --git a/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java b/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java new file mode 100644 index 0000000000..7a49957f39 --- /dev/null +++ b/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.thread.cts; + +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; + +import static com.google.common.io.BaseEncoding.base16; +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.net.IpPrefix; +import android.net.thread.ActiveOperationalDataset; +import android.net.thread.OperationalDatasetTimestamp; +import android.net.thread.PendingOperationalDataset; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.google.common.primitives.Bytes; +import com.google.common.testing.EqualsTester; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.time.Duration; + +/** Tests for {@link PendingOperationalDataset}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class PendingOperationalDatasetTest { + private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET = + ActiveOperationalDataset.createRandomDataset(); + + @Test + public void parcelable_parcelingIsLossLess() { + PendingOperationalDataset dataset = + new PendingOperationalDataset( + DEFAULT_ACTIVE_DATASET, + new OperationalDatasetTimestamp(31536000, 200, false), + Duration.ofHours(100)); + + assertParcelingIsLossless(dataset); + } + + @Test + public void equalityTests() { + ActiveOperationalDataset activeDataset1 = ActiveOperationalDataset.createRandomDataset(); + ActiveOperationalDataset activeDataset2 = ActiveOperationalDataset.createRandomDataset(); + + new EqualsTester() + .addEqualityGroup( + new PendingOperationalDataset( + activeDataset1, + new OperationalDatasetTimestamp(31536000, 100, false), + Duration.ofMillis(0)), + new PendingOperationalDataset( + activeDataset1, + new OperationalDatasetTimestamp(31536000, 100, false), + Duration.ofMillis(0))) + .addEqualityGroup( + new PendingOperationalDataset( + activeDataset2, + new OperationalDatasetTimestamp(31536000, 100, false), + Duration.ofMillis(0)), + new PendingOperationalDataset( + activeDataset2, + new OperationalDatasetTimestamp(31536000, 100, false), + Duration.ofMillis(0))) + .addEqualityGroup( + new PendingOperationalDataset( + activeDataset2, + new OperationalDatasetTimestamp(15768000, 0, false), + Duration.ofMillis(0)), + new PendingOperationalDataset( + activeDataset2, + new OperationalDatasetTimestamp(15768000, 0, false), + Duration.ofMillis(0))) + .addEqualityGroup( + new PendingOperationalDataset( + activeDataset2, + new OperationalDatasetTimestamp(15768000, 0, false), + Duration.ofMillis(100)), + new PendingOperationalDataset( + activeDataset2, + new OperationalDatasetTimestamp(15768000, 0, false), + Duration.ofMillis(100))) + .testEquals(); + } + + @Test + public void constructor_correctValuesAreSet() { + PendingOperationalDataset dataset = + new PendingOperationalDataset( + DEFAULT_ACTIVE_DATASET, + new OperationalDatasetTimestamp(31536000, 200, false), + Duration.ofHours(100)); + + assertThat(dataset.getActiveOperationalDataset()).isEqualTo(DEFAULT_ACTIVE_DATASET); + assertThat(dataset.getPendingTimestamp()) + .isEqualTo(new OperationalDatasetTimestamp(31536000, 200, false)); + assertThat(dataset.getDelayTimer()).isEqualTo(Duration.ofHours(100)); + } + + @Test + public void fromThreadTlvs_openthreadTlvs_success() { + // An example Pending Operational Dataset which is generated with OpenThread CLI: + // Pending Timestamp: 2 + // Active Timestamp: 1 + // Channel: 26 + // Channel Mask: 0x07fff800 + // Delay: 46354 + // Ext PAN ID: a74182f4d3f4de41 + // Mesh Local Prefix: fd46:c1b9:e159:5574::/64 + // Network Key: ed916e454d96fd00184f10a6f5c9e1d3 + // Network Name: OpenThread-bff8 + // PAN ID: 0xbff8 + // PSKc: 264f78414adc683191863d968f72d1b7 + // Security Policy: 672 onrc + final byte[] OPENTHREAD_PENDING_DATASET_TLVS = + base16().lowerCase() + .decode( + "0e0800000000000100003308000000000002000034040000b51200030000" + + "1a35060004001fffe00208a74182f4d3f4de410708fd46c1b9" + + "e15955740510ed916e454d96fd00184f10a6f5c9e1d3030f4f" + + "70656e5468726561642d626666380102bff80410264f78414a" + + "dc683191863d968f72d1b70c0402a0f7f8"); + + PendingOperationalDataset pendingDataset = + PendingOperationalDataset.fromThreadTlvs(OPENTHREAD_PENDING_DATASET_TLVS); + + ActiveOperationalDataset activeDataset = pendingDataset.getActiveOperationalDataset(); + assertThat(pendingDataset.getPendingTimestamp().getSeconds()).isEqualTo(2L); + assertThat(activeDataset.getActiveTimestamp().getSeconds()).isEqualTo(1L); + assertThat(activeDataset.getChannel()).isEqualTo(26); + assertThat(activeDataset.getChannelMask().get(0)) + .isEqualTo(new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0}); + assertThat(pendingDataset.getDelayTimer().toMillis()).isEqualTo(46354); + assertThat(activeDataset.getExtendedPanId()) + .isEqualTo(base16().lowerCase().decode("a74182f4d3f4de41")); + assertThat(activeDataset.getMeshLocalPrefix()) + .isEqualTo(new IpPrefix("fd46:c1b9:e159:5574::/64")); + assertThat(activeDataset.getNetworkKey()) + .isEqualTo(base16().lowerCase().decode("ed916e454d96fd00184f10a6f5c9e1d3")); + assertThat(activeDataset.getNetworkName()).isEqualTo("OpenThread-bff8"); + assertThat(activeDataset.getPanId()).isEqualTo(0xbff8); + assertThat(activeDataset.getPskc()) + .isEqualTo(base16().lowerCase().decode("264f78414adc683191863d968f72d1b7")); + assertThat(activeDataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672); + assertThat(activeDataset.getSecurityPolicy().getFlags()) + .isEqualTo(new byte[] {(byte) 0xf7, (byte) 0xf8}); + } + + @Test + public void fromThreadTlvs_completePendingDatasetTlvs_success() { + // Type Length Value + // 0x33 0x08 0x0000000000010000 (Pending Timestamp TLV) + // 0x34 0x04 0x0000012C (Delay Timer TLV) + final byte[] pendingTimestampAndDelayTimerTlvs = + base16().decode("3308000000000001000034040000012C"); + final byte[] pendingDatasetTlvs = + Bytes.concat( + pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs()); + + PendingOperationalDataset dataset = + PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs); + + assertThat(dataset.getActiveOperationalDataset()).isEqualTo(DEFAULT_ACTIVE_DATASET); + assertThat(dataset.getPendingTimestamp()) + .isEqualTo(new OperationalDatasetTimestamp(1, 0, false)); + assertThat(dataset.getDelayTimer()).isEqualTo(Duration.ofMillis(300)); + } + + @Test + public void fromThreadTlvs_PendingTimestampTlvIsMissing_throwsIllegalArgument() { + // Type Length Value + // 0x34 0x04 0x00000064 (Delay Timer TLV) + final byte[] pendingTimestampAndDelayTimerTlvs = base16().decode("34040000012C"); + final byte[] pendingDatasetTlvs = + Bytes.concat( + pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs()); + + assertThrows( + IllegalArgumentException.class, + () -> PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs)); + } + + @Test + public void fromThreadTlvs_delayTimerTlvIsMissing_throwsIllegalArgument() { + // Type Length Value + // 0x33 0x08 0x0000000000010000 (Pending Timestamp TLV) + final byte[] pendingTimestampAndDelayTimerTlvs = base16().decode("33080000000000010000"); + final byte[] pendingDatasetTlvs = + Bytes.concat( + pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs()); + + assertThrows( + IllegalArgumentException.class, + () -> PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs)); + } + + @Test + public void fromThreadTlvs_activeDatasetTlvs_throwsIllegalArgument() { + final byte[] activeDatasetTlvs = DEFAULT_ACTIVE_DATASET.toThreadTlvs(); + + assertThrows( + IllegalArgumentException.class, + () -> PendingOperationalDataset.fromThreadTlvs(activeDatasetTlvs)); + } + + @Test + public void fromThreadTlvs_malformedTlvs_throwsIllegalArgument() { + final byte[] invalidTlvs = new byte[] {0x00}; + + assertThrows( + IllegalArgumentException.class, + () -> PendingOperationalDataset.fromThreadTlvs(invalidTlvs)); + } + + @Test + public void toThreadTlvs_conversionIsLossLess() { + PendingOperationalDataset dataset1 = + new PendingOperationalDataset( + DEFAULT_ACTIVE_DATASET, + new OperationalDatasetTimestamp(31536000, 200, false), + Duration.ofHours(100)); + + PendingOperationalDataset dataset2 = + PendingOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs()); + + assertThat(dataset2).isEqualTo(dataset1); + } +} diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp new file mode 100644 index 0000000000..1f16ad1b65 --- /dev/null +++ b/thread/tests/unit/Android.bp @@ -0,0 +1,50 @@ +// +// Copyright (C) 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_test { + name: "ThreadNetworkUnitTests", + min_sdk_version: "33", + sdk_version: "module_current", + manifest: "AndroidManifest.xml", + test_config: "AndroidTest.xml", + srcs: [ + "src/**/*.java", + ], + test_suites: [ + "general-tests", + ], + static_libs: [ + "androidx.test.ext.junit", + "compatibility-device-util-axt", + "ctstestrunner-axt", + "framework-connectivity-pre-jarjar", + "framework-connectivity-t-pre-jarjar", + "guava-android-testlib", + "net-tests-utils", + "truth-prebuilt", + ], + libs: [ + "android.test.base", + "android.test.runner", + ], + // Test coverage system runs on different devices. Need to + // compile for all architectures. + compile_multilib: "both", +} diff --git a/thread/tests/unit/AndroidManifest.xml b/thread/tests/unit/AndroidManifest.xml new file mode 100644 index 0000000000..ace7c52878 --- /dev/null +++ b/thread/tests/unit/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml new file mode 100644 index 0000000000..663ff7455e --- /dev/null +++ b/thread/tests/unit/AndroidTest.xml @@ -0,0 +1,33 @@ + + + + + diff --git a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java new file mode 100644 index 0000000000..78eb3d03fe --- /dev/null +++ b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.thread; + +import static com.google.common.io.BaseEncoding.base16; +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.net.IpPrefix; +import android.net.thread.ActiveOperationalDataset.Builder; +import android.net.thread.ActiveOperationalDataset.SecurityPolicy; +import android.util.SparseArray; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.google.common.primitives.Bytes; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.security.SecureRandom; +import java.util.Random; + +/** Unit tests for {@link ActiveOperationalDataset}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ActiveOperationalDatasetTest { + // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new": + // Active Timestamp: 1 + // Channel: 19 + // Channel Mask: 0x07FFF800 + // Ext PAN ID: ACC214689BC40BDF + // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64 + // Network Key: F26B3153760F519A63BAFDDFFC80D2AF + // Network Name: OpenThread-d9a0 + // PAN ID: 0xD9A0 + // PSKc: A245479C836D551B9CA557F7B9D351B4 + // Security Policy: 672 onrcb + private static final byte[] VALID_DATASET = + base16().decode( + "0E080000000000010000000300001335060004001FFFE002" + + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31" + + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561" + + "642D643961300102D9A00410A245479C836D551B9CA557F7" + + "B9D351B40C0402A0FFF8"); + + @Mock private Random mockRandom; + @Mock private SecureRandom mockSecureRandom; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + private static byte[] addTlv(byte[] dataset, String tlvHex) { + return Bytes.concat(dataset, base16().decode(tlvHex)); + } + + @Test + public void fromThreadTlvs_containsUnknownTlvs_unknownTlvsRetained() { + byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET, "AA01FFBB020102"); + + ActiveOperationalDataset dataset1 = + ActiveOperationalDataset.fromThreadTlvs(datasetWithUnknownTlvs); + ActiveOperationalDataset dataset2 = + ActiveOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs()); + + SparseArray unknownTlvs = dataset2.getUnknownTlvs(); + assertThat(unknownTlvs.size()).isEqualTo(2); + assertThat(unknownTlvs.get(0xAA)).isEqualTo(new byte[] {(byte) 0xFF}); + assertThat(unknownTlvs.get(0xBB)).isEqualTo(new byte[] {0x01, 0x02}); + assertThat(dataset2).isEqualTo(dataset1); + } + + @Test + public void createRandomDataset_fieldsAreRandomized() { + // Always return the max bounded value + doAnswer(invocation -> (int) invocation.getArgument(0) - 1) + .when(mockRandom) + .nextInt(anyInt()); + doAnswer( + invocation -> { + byte[] output = invocation.getArgument(0); + for (int i = 0; i < output.length; ++i) { + output[i] = (byte) (i + 10); + } + return null; + }) + .when(mockRandom) + .nextBytes(any(byte[].class)); + doAnswer( + invocation -> { + byte[] output = invocation.getArgument(0); + for (int i = 0; i < output.length; ++i) { + output[i] = (byte) (i + 30); + } + return null; + }) + .when(mockSecureRandom) + .nextBytes(any(byte[].class)); + + ActiveOperationalDataset dataset = + ActiveOperationalDataset.createRandomDataset(mockRandom, mockSecureRandom); + + assertThat(dataset.getActiveTimestamp()) + .isEqualTo(new OperationalDatasetTimestamp(1, 0, false)); + assertThat(dataset.getExtendedPanId()) + .isEqualTo(new byte[] {10, 11, 12, 13, 14, 15, 16, 17}); + assertThat(dataset.getMeshLocalPrefix()) + .isEqualTo(new IpPrefix("fd0b:0c0d:0e0f:1011::/64")); + verify(mockRandom, times(2)).nextBytes(any(byte[].class)); + assertThat(dataset.getPanId()).isEqualTo(0xfffe); // PAN ID <= 0xfffe + verify(mockRandom, times(1)).nextInt(eq(0xffff)); + assertThat(dataset.getChannel()).isEqualTo(26); + verify(mockRandom, times(1)).nextInt(eq(16)); + assertThat(dataset.getChannelPage()).isEqualTo(0); + assertThat(dataset.getChannelMask().size()).isEqualTo(1); + assertThat(dataset.getPskc()) + .isEqualTo( + new byte[] { + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45 + }); + assertThat(dataset.getNetworkKey()) + .isEqualTo( + new byte[] { + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45 + }); + verify(mockSecureRandom, times(2)).nextBytes(any(byte[].class)); + assertThat(dataset.getSecurityPolicy()) + .isEqualTo(new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8})); + } + + @Test + public void builder_buildWithTooLongTlvs_throwsIllegalState() { + Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset()); + for (int i = 0; i < 10; i++) { + builder.addUnknownTlv(i, new byte[20]); + } + + assertThrows(IllegalStateException.class, () -> new Builder().build()); + } + + @Test + public void builder_setUnknownTlvs_success() { + ActiveOperationalDataset dataset1 = ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET); + SparseArray unknownTlvs = new SparseArray<>(2); + unknownTlvs.put(0x33, new byte[] {1, 2, 3}); + unknownTlvs.put(0x44, new byte[] {1, 2, 3, 4}); + + ActiveOperationalDataset dataset2 = + new ActiveOperationalDataset.Builder(dataset1).setUnknownTlvs(unknownTlvs).build(); + + assertThat(dataset1.getUnknownTlvs().size()).isEqualTo(0); + assertThat(dataset2.getUnknownTlvs().size()).isEqualTo(2); + assertThat(dataset2.getUnknownTlvs().get(0x33)).isEqualTo(new byte[] {1, 2, 3}); + assertThat(dataset2.getUnknownTlvs().get(0x44)).isEqualTo(new byte[] {1, 2, 3, 4}); + } + + @Test + public void securityPolicy_fromTooShortTlvValue_throwsIllegalArgument() { + assertThrows( + IllegalArgumentException.class, + () -> SecurityPolicy.fromTlvValue(new byte[] {0x01})); + assertThrows( + IllegalArgumentException.class, + () -> SecurityPolicy.fromTlvValue(new byte[] {0x01, 0x02})); + } + + @Test + public void securityPolicy_toTlvValue_conversionIsLossLess() { + SecurityPolicy policy1 = new SecurityPolicy(200, new byte[] {(byte) 0xFF, (byte) 0xF8}); + + SecurityPolicy policy2 = SecurityPolicy.fromTlvValue(policy1.toTlvValue()); + + assertThat(policy2).isEqualTo(policy1); + } +} diff --git a/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java new file mode 100644 index 0000000000..32063fc89a --- /dev/null +++ b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.thread; + +import static com.google.common.io.BaseEncoding.base16; +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link OperationalDatasetTimestamp}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class OperationalDatasetTimestampTest { + @Test + public void fromTlvValue_invalidTimestamp_throwsIllegalArguments() { + assertThrows( + IllegalArgumentException.class, + () -> OperationalDatasetTimestamp.fromTlvValue(new byte[7])); + } + + @Test + public void fromTlvValue_goodValue_success() { + OperationalDatasetTimestamp timestamp = + OperationalDatasetTimestamp.fromTlvValue(base16().decode("FFEEDDCCBBAA9989")); + + assertThat(timestamp.getSeconds()).isEqualTo(0xFFEEDDCCBBAAL); + // 0x9989 is 0x4CC4 << 1 + 1 + assertThat(timestamp.getTicks()).isEqualTo(0x4CC4); + assertThat(timestamp.isAuthoritativeSource()).isTrue(); + } + + @Test + public void toTlvValue_conversionIsLossLess() { + OperationalDatasetTimestamp timestamp1 = new OperationalDatasetTimestamp(100L, 10, true); + + OperationalDatasetTimestamp timestamp2 = + OperationalDatasetTimestamp.fromTlvValue(timestamp1.toTlvValue()); + + assertThat(timestamp2).isEqualTo(timestamp1); + } +}