/* * 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. * *

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

{@code
 * ActiveOperationalDataset activeDataset = controller.createRandomizedDataset("MyNet");
 * }
* *

or randomized Dataset with customized channel: * *

{@code
 * ActiveOperationalDataset activeDataset =
 *         new ActiveOperationalDataset.Builder(controller.createRandomizedDataset("MyNet"))
 *                 .setChannel(CHANNEL_PAGE_24_GHZ, 17)
 *                 .setActiveTimestamp(OperationalDatasetTimestamp.fromInstant(Instant.now()))
 *                 .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; /** @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 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); } 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(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 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) { this.mNetworkName = checkNetworkName(networkName); return this; } /** * Sets the Extended PAN ID. * *

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. * *

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 * 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. * *

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