1118 lines
42 KiB
Java
1118 lines
42 KiB
Java
/*
|
|
* 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 com.android.internal.util.Preconditions.checkState;
|
|
import static com.android.net.module.util.HexDump.toHexString;
|
|
|
|
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.util.Arrays;
|
|
|
|
/**
|
|
* Data interface for managing a Thread Active Operational Dataset.
|
|
*
|
|
* <p>An example usage of creating an Active Operational Dataset with randomized parameters:
|
|
*
|
|
* <pre>{@code
|
|
* ActiveOperationalDataset activeDataset = controller.createRandomizedDataset("MyNet");
|
|
* }</pre>
|
|
*
|
|
* <p>or randomized Dataset with customized channel:
|
|
*
|
|
* <pre>{@code
|
|
* ActiveOperationalDataset activeDataset =
|
|
* new ActiveOperationalDataset.Builder(controller.createRandomizedDataset("MyNet"))
|
|
* .setChannel(CHANNEL_PAGE_24_GHZ, 17)
|
|
* .setActiveTimestamp(OperationalDatasetTimestamp.fromInstant(Instant.now()))
|
|
* .build();
|
|
* }</pre>
|
|
*
|
|
* <p>If the Active Operational Dataset is already known as <a
|
|
* href="https://www.threadgroup.org">Thread TLVs</a>, you can simply use:
|
|
*
|
|
* <pre>{@code
|
|
* ActiveOperationalDataset activeDataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlvs);
|
|
* }</pre>
|
|
*
|
|
* @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;
|
|
|
|
/** @hide */
|
|
public 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<ActiveOperationalDataset> 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<byte[]> mChannelMask;
|
|
private final byte[] mPskc;
|
|
private final byte[] mNetworkKey;
|
|
private final IpPrefix mMeshLocalPrefix;
|
|
private final SecurityPolicy mSecurityPolicy;
|
|
private final SparseArray<byte[]> 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<byte[]> channelMask,
|
|
byte[] pskc,
|
|
byte[] networkKey,
|
|
IpPrefix meshLocalPrefix,
|
|
SecurityPolicy securityPolicy,
|
|
SparseArray<byte[]> 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.
|
|
*
|
|
* <p>{@code tlvs} can be obtained from the value of a Thread Active Operational Dataset TLV
|
|
* (see the <a href="https://www.threadgroup.org/support#specifications">Thread
|
|
* specification</a> 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<byte[]> decodeChannelMask(byte[] tlvValue) {
|
|
SparseArray<byte[]> 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<byte[]> 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);
|
|
}
|
|
|
|
private static boolean areByteSparseArraysEqual(
|
|
@NonNull SparseArray<byte[]> first, @NonNull SparseArray<byte[]> 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.
|
|
*
|
|
* <p>See the <a href="https://www.threadgroup.org/support#specifications">Thread
|
|
* specification</a> 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<byte[]> getChannelMask() {
|
|
return deepCloneSparseArray(mChannelMask);
|
|
}
|
|
|
|
private static SparseArray<byte[]> deepCloneSparseArray(SparseArray<byte[]> src) {
|
|
SparseArray<byte[]> 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<byte[]> 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(toHexString(getExtendedPanId()))
|
|
.append(", panId=")
|
|
.append(getPanId())
|
|
.append(", channel=")
|
|
.append(getChannel())
|
|
.append(", activeTimestamp=")
|
|
.append(getActiveTimestamp())
|
|
.append("}");
|
|
return sb.toString();
|
|
}
|
|
|
|
static String checkNetworkName(@NonNull 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);
|
|
return networkName;
|
|
}
|
|
|
|
/** 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<byte[]> mChannelMask;
|
|
private byte[] mPskc;
|
|
private byte[] mNetworkKey;
|
|
private IpPrefix mMeshLocalPrefix;
|
|
private SecurityPolicy mSecurityPolicy;
|
|
private SparseArray<byte[]> 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.
|
|
*
|
|
* <p>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) {
|
|
this.mNetworkName = checkNetworkName(networkName);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the Extended PAN ID.
|
|
*
|
|
* <p>Use with caution. A randomized 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 ThreadNetworkController#createRandomizedDataset} 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.
|
|
*
|
|
* <p>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<byte[]> channelMask) {
|
|
requireNonNull(channelMask, "channelMask cannot be null");
|
|
checkArgument(channelMask.size() > 0, "channelMask is empty");
|
|
this.mChannelMask = deepCloneSparseArray(channelMask);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the PSKc.
|
|
*
|
|
* <p>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
|
|
* ThreadNetworkController#createRandomizedDataset} 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.
|
|
*
|
|
* <p>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 ThreadNetworkController#createRandomizedDataset} 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;
|
|
}
|
|
|
|
/**
|
|
* Sets the Mesh-Local Prefix.
|
|
*
|
|
* @param meshLocalPrefix the prefix used for realm-local traffic within the mesh
|
|
* @throws IllegalArgumentException if {@code meshLocalPrefix} doesn't start with {@code
|
|
* 0xfd} or has length other than {@code LENGTH_MESH_LOCAL_PREFIX_BITS / 8}
|
|
* @hide
|
|
*/
|
|
@NonNull
|
|
public 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<byte[]> 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(toHexString(mFlags))
|
|
.append("}");
|
|
return sb.toString();
|
|
}
|
|
}
|
|
}
|