blob: 18b19afea60f234001003b7c5d5a8c4a068d12bc [file] [log] [blame]
/*
*
* Copyright (c) 2021-2022 Project CHIP Authors
*
* 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.
*/
#pragma once
#include <optional>
#include <stdint.h>
#include <sys/types.h>
#include <app/util/basic-types.h>
#include <crypto/CHIPCryptoPAL.h>
#include <lib/core/CHIPError.h>
#include <lib/core/ClusterEnums.h>
#include <lib/support/CHIPMemString.h>
#include <lib/support/CommonIterator.h>
namespace chip {
namespace Credentials {
class GroupDataProvider
{
public:
using SecurityPolicy = app::Clusters::GroupKeyManagement::GroupKeySecurityPolicyEnum;
static constexpr KeysetId kIdentityProtectionKeySetId = 0;
static constexpr size_t kMaxListeners = 2;
struct GroupInfo
{
static constexpr size_t kGroupNameMax = CHIP_CONFIG_MAX_GROUP_NAME_LENGTH;
enum class Flags : uint8_t
{
kHasAuxiliaryACL = 0b00000001,
kMcastAddrPolicy = 0b00000010,
};
static constexpr uint8_t kFlagsDefault = to_underlying(GroupInfo::Flags::kMcastAddrPolicy);
// Identifies group within the scope of the given Fabric
GroupId group_id = kUndefinedGroupId;
// Lastest group name written for a given GroupId on any Endpoint via the Groups cluster
char name[kGroupNameMax + 1] = { 0 };
uint8_t flags = kFlagsDefault;
uint16_t count = 0;
GroupInfo() { SetName(nullptr); }
GroupInfo(const GroupInfo & other) { Copy(other); }
GroupInfo(const char * groupName) { SetName(groupName); }
GroupInfo(const CharSpan & groupName) { SetName(groupName); }
GroupInfo(GroupId id, const char * groupName, uint8_t groupFlags = kFlagsDefault) : group_id(id), flags(groupFlags)
{
SetName(groupName);
}
GroupInfo(GroupId id, const CharSpan & groupName, uint8_t groupFlags = kFlagsDefault) : group_id(id), flags(groupFlags)
{
SetName(groupName);
}
void SetName(const char * groupName)
{
if (nullptr == groupName)
{
name[0] = 0;
}
else
{
Platform::CopyString(name, groupName);
}
}
void SetName(const CharSpan & groupName)
{
if (nullptr == groupName.data())
{
name[0] = 0;
}
else
{
Platform::CopyString(name, groupName);
}
}
void Copy(const GroupInfo & other)
{
if (this != &other)
{
group_id = other.group_id;
flags = other.flags;
SetName(other.name);
}
}
bool HasAuxiliaryACL() const { return (flags & static_cast<uint8_t>(Flags::kHasAuxiliaryACL)); }
bool UsePerGroupAddress() const { return (flags & static_cast<uint8_t>(Flags::kMcastAddrPolicy)); }
bool operator==(const GroupInfo & other) const
{
return (this->group_id == other.group_id) && !strncmp(this->name, other.name, kGroupNameMax);
}
GroupInfo & operator=(const GroupInfo & other)
{
Copy(other);
return *this;
}
};
struct GroupKey
{
GroupKey() = default;
GroupKey(GroupId group, KeysetId keyset) : group_id(group), keyset_id(keyset) {}
// Identifies group within the scope of the given Fabric
GroupId group_id = kUndefinedGroupId;
// Set of group keys that generate operational group keys for use with this group
KeysetId keyset_id = 0;
bool operator==(const GroupKey & other) const
{
return this->group_id == other.group_id && this->keyset_id == other.keyset_id;
}
};
struct GroupEndpoint
{
GroupEndpoint() = default;
GroupEndpoint(GroupId group, EndpointId endpoint) : group_id(group), endpoint_id(endpoint) {}
// Identifies group within the scope of the given Fabric
GroupId group_id = kUndefinedGroupId;
// Endpoint on the Node to which messages to this group may be forwarded
EndpointId endpoint_id = kInvalidEndpointId;
bool operator==(const GroupEndpoint & other) const
{
return this->group_id == other.group_id && this->endpoint_id == other.endpoint_id;
}
};
struct GroupSession
{
GroupSession() = default;
GroupId group_id = kUndefinedGroupId;
FabricIndex fabric_index;
SecurityPolicy security_policy;
Crypto::SymmetricKeyContext * keyContext = nullptr;
};
// An EpochKey is a single key usable to determine an operational group key
struct EpochKey
{
static constexpr size_t kLengthBytes = Crypto::CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES;
// Validity start time in microseconds since 2000-01-01T00:00:00 UTC ("the Epoch")
uint64_t start_time;
// Actual key bits. Depending on context, it may be a raw epoch key (as seen within `SetKeySet` calls)
// or it may be the derived operational group key (as seen in any other usage).
uint8_t key[kLengthBytes];
void Clear()
{
start_time = 0;
Crypto::ClearSecretData(&key[0], sizeof(key));
}
};
// A operational group key set, usable by many GroupState mappings
struct KeySet
{
static constexpr size_t kEpochKeysMax = 3;
KeySet() = default;
KeySet(uint16_t id, SecurityPolicy policy_id, uint8_t num_keys) : keyset_id(id), policy(policy_id), num_keys_used(num_keys)
{}
// The actual keys for the group key set
EpochKey epoch_keys[kEpochKeysMax];
// Logical id provided by the Administrator that configured the entry
uint16_t keyset_id = 0;
// Security policy to use for groups that use this keyset
SecurityPolicy policy = SecurityPolicy::kCacheAndSync;
// Number of keys present
uint8_t num_keys_used = 0;
bool operator==(const KeySet & other) const
{
VerifyOrReturnError(this->policy == other.policy && this->num_keys_used == other.num_keys_used, false);
return !memcmp(this->epoch_keys, other.epoch_keys, this->num_keys_used * sizeof(EpochKey));
}
void ClearKeys()
{
for (size_t key_idx = 0; key_idx < kEpochKeysMax; ++key_idx)
{
epoch_keys[key_idx].Clear();
}
}
};
/**
* Interface to listen for changes in the Group info.
*/
class GroupListener
{
public:
virtual ~GroupListener() = default;
/**
* Callback invoked when a new group is added.
*
* @param[in] new_group GroupInfo structure of the new group.
*/
virtual void OnGroupAdded(FabricIndex fabric_index, const GroupInfo & new_group) = 0;
/**
* Callback invoked when an existing group is removed.
*
* @param[in] old_group GroupInfo structure of the removed group.
*/
virtual void OnGroupRemoved(FabricIndex fabric_index, const GroupInfo & old_group) = 0;
/**
* Callback invoked when an existing group is modified.
* The modifications may be any of the following:
* - Endpoints List modified
* - KeySetID modified
* - Flags modified (kHasAuxiliaryACL or kMcastAddrPolicy)
*
* Note that this callback is not invoked when the group is added or removed.
* Those events are handled by the OnGroupAdded and OnGroupRemoved callbacks respectively.
*
* @param[in] modified_group_id ID of the modified group.
*/
virtual void OnGroupModified(FabricIndex fabric_index, const GroupId & modified_group_id){};
};
using GroupInfoIterator = CommonIterator<GroupInfo>;
using GroupKeyIterator = CommonIterator<GroupKey>;
using EndpointIterator = CommonIterator<GroupEndpoint>;
using KeySetIterator = CommonIterator<KeySet>;
using GroupSessionIterator = CommonIterator<GroupSession>;
GroupDataProvider(uint16_t maxGroupsPerFabric, uint16_t maxGroupKeysPerFabric) :
mMaxGroupsPerFabric(maxGroupsPerFabric), mMaxGroupKeysPerFabric(maxGroupKeysPerFabric)
{}
enum class GroupCleanupPolicy
{
kDeleteGroupIfEmpty, // Default behavior for legacy Groups
kKeepGroupIfEmpty // Required for Groupcast Sender feature
};
virtual ~GroupDataProvider() = default;
// Not copyable
GroupDataProvider(const GroupDataProvider &) = delete;
GroupDataProvider & operator=(const GroupDataProvider &) = delete;
uint16_t GetMaxGroupsPerFabric() const { return mMaxGroupsPerFabric; }
uint16_t GetMaxGroupKeysPerFabric() const { return mMaxGroupKeysPerFabric; }
/**
* Initialize the GroupDataProvider, including possibly any persistent
* data store initialization done by the implementation. Must be called once
* before any other API succeeds.
*
* @retval #CHIP_ERROR_INCORRECT_STATE if called when already initialized.
* @retval #CHIP_NO_ERROR on success
*/
virtual CHIP_ERROR Init() = 0;
virtual void Finish() = 0;
//
// Group Table
//
// By id
virtual CHIP_ERROR SetGroupInfo(FabricIndex fabric_index, const GroupInfo & info) = 0;
virtual CHIP_ERROR GetGroupInfo(FabricIndex fabric_index, GroupId group_id, GroupInfo & info) = 0;
virtual CHIP_ERROR RemoveGroupInfo(FabricIndex fabric_index, GroupId group_id) = 0;
// By index
virtual CHIP_ERROR SetGroupInfoAt(FabricIndex fabric_index, size_t index, const GroupInfo & info) = 0;
virtual CHIP_ERROR GetGroupInfoAt(FabricIndex fabric_index, size_t index, GroupInfo & info) = 0;
virtual CHIP_ERROR RemoveGroupInfoAt(FabricIndex fabric_index, size_t index) = 0;
// Endpoints
virtual bool HasEndpoint(FabricIndex fabric_index, GroupId group_id, EndpointId endpoint_id) = 0;
virtual CHIP_ERROR AddEndpoint(FabricIndex fabric_index, GroupId group_id, EndpointId endpoint_id) = 0;
virtual CHIP_ERROR RemoveEndpoint(FabricIndex fabric_index, GroupId group_id, EndpointId endpoint_id,
GroupCleanupPolicy cleanupPolicy) = 0;
virtual CHIP_ERROR RemoveEndpointAllGroups(FabricIndex fabric_index, EndpointId endpoint_id,
GroupCleanupPolicy cleanupPolicy) = 0;
virtual CHIP_ERROR RemoveEndpoint(FabricIndex fabric_index, GroupId group_id, EndpointId endpoint_id) = 0;
virtual CHIP_ERROR RemoveEndpoint(FabricIndex fabric_index, EndpointId endpoint_id) = 0;
virtual CHIP_ERROR RemoveEndpoints(FabricIndex fabric_index, GroupId group_id) = 0;
// Iterators
/**
* Creates an iterator that may be used to obtain the list of groups associated with the given fabric.
* In order to release the allocated memory, the Release() method must be called after the iteration is finished.
* Modifying the group table during the iteration is currently not supported, and may yield unexpected behaviour.
* @retval An instance of EndpointIterator on success
* @retval nullptr if no iterator instances are available.
*/
virtual GroupInfoIterator * IterateGroupInfo(FabricIndex fabric_index) = 0;
/**
* Creates an iterator that may be used to obtain the list of (group, endpoint) pairs associated with the given fabric.
* In order to release the allocated memory, the Release() method must be called after the iteration is finished.
* Modifying the group table during the iteration is currently not supported, and may yield unexpected behaviour.
* If you wish to iterate only the endpoints of a particular group id you can provide the optional `group_id` to do so.
* @retval An instance of EndpointIterator on success
* @retval nullptr if no iterator instances are available.
*/
virtual EndpointIterator * IterateEndpoints(FabricIndex fabric_index, std::optional<GroupId> group_id = std::nullopt) = 0;
//
// Group-Key map
//
virtual CHIP_ERROR SetGroupKey(FabricIndex fabric_index, GroupId group_id, KeysetId keyset_id) = 0;
virtual CHIP_ERROR SetGroupKeyAt(FabricIndex fabric_index, size_t index, const GroupKey & info) = 0;
virtual CHIP_ERROR GetGroupKey(FabricIndex fabric_index, GroupId group_id, KeysetId & keyset_id) = 0;
virtual CHIP_ERROR GetGroupKeyAt(FabricIndex fabric_index, size_t index, GroupKey & info) = 0;
virtual CHIP_ERROR RemoveGroupKeyAt(FabricIndex fabric_index, size_t index) = 0;
virtual CHIP_ERROR RemoveGroupKeys(FabricIndex fabric_index) = 0;
/**
* Creates an iterator that may be used to obtain the list of (group, keyset) pairs associated with the given fabric.
* In order to release the allocated memory, the Release() method must be called after the iteration is finished.
* Modifying the keyset mappings during the iteration is currently not supported, and may yield unexpected behaviour.
* @retval An instance of GroupKeyIterator on success
* @retval nullptr if no iterator instances are available.
*/
virtual GroupKeyIterator * IterateGroupKeys(FabricIndex fabric_index) = 0;
//
// Key Sets
//
virtual CHIP_ERROR SetKeySet(FabricIndex fabric_index, const ByteSpan & compressed_fabric_id, const KeySet & keys) = 0;
virtual CHIP_ERROR GetKeySet(FabricIndex fabric_index, KeysetId keyset_id, KeySet & keys) = 0;
virtual CHIP_ERROR RemoveKeySet(FabricIndex fabric_index, KeysetId keyset_id) = 0;
/**
* @brief Obtain the actual operational Identity Protection Key (IPK) keyset for a given
* fabric. These keys are used by the CASE protocol, and do not participate in
* any direct traffic encryption. Since the identity protection operational keyset
* is used in multiple key derivations and procedures, it cannot be hidden behind a
* SymmetricKeyContext, and must be obtainable by value.
*
* @param fabric_index - Fabric index for which to get the IPK operational keyset
* @param out_keyset - Reference to a KeySet where the IPK keys will be stored on success
* @return CHIP_NO_ERROR on success, CHIP_ERROR_NOT_FOUND if the IPK keyset is somehow unavailable
* or another CHIP_ERROR value if an internal storage error occurs.
*/
virtual CHIP_ERROR GetIpkKeySet(FabricIndex fabric_index, KeySet & out_keyset) = 0;
/**
* Creates an iterator that may be used to obtain the list of key sets associated with the given fabric.
* In order to release the allocated memory, the Release() method must be called after the iteration is finished.
* Modifying the key sets table during the iteration is currently not supported, and may yield unexpected behaviour.
*
* @retval An instance of KeySetIterator on success
* @retval nullptr if no iterator instances are available.
*/
virtual KeySetIterator * IterateKeySets(FabricIndex fabric_index) = 0;
// Fabrics
virtual CHIP_ERROR RemoveFabric(FabricIndex fabric_index) = 0;
// Decryption
virtual GroupSessionIterator * IterateGroupSessions(uint16_t session_id) = 0;
virtual Crypto::SymmetricKeyContext * GetKeyContext(FabricIndex fabric_index, GroupId group_id) = 0;
// Listener
void SetListener(GroupListener * listener)
{
for (size_t i = 0; listener && (i < kMaxListeners); ++i)
{
if (nullptr == mListeners[i])
{
mListeners[i] = listener;
return;
}
}
}
void RemoveListener(GroupListener * listener)
{
for (size_t i = 0; listener && (i < kMaxListeners); ++i)
{
if (listener == mListeners[i])
{
mListeners[i] = nullptr;
return;
}
}
}
// Groupcast
virtual uint16_t getMaxMembershipCount() = 0;
virtual uint16_t getMaxMcastAddrCount() = 0;
protected:
void GroupAdded(FabricIndex fabric_index, const GroupInfo & new_group)
{
for (auto * listener : mListeners)
{
if (listener != nullptr)
{
listener->OnGroupAdded(fabric_index, new_group);
}
}
}
void GroupRemoved(FabricIndex fabric_index, const GroupInfo & old_group)
{
for (auto * listener : mListeners)
{
if (listener != nullptr)
{
listener->OnGroupRemoved(fabric_index, old_group);
}
}
}
void GroupModified(FabricIndex fabric_index, const GroupId & modified_group_id)
{
for (auto * listener : mListeners)
{
if (listener != nullptr)
{
listener->OnGroupModified(fabric_index, modified_group_id);
}
}
}
const uint16_t mMaxGroupsPerFabric;
const uint16_t mMaxGroupKeysPerFabric;
GroupListener * mListeners[kMaxListeners] = { nullptr };
};
/**
* @brief Utility Set the IPK Epoch key on a GroupDataProvider assuming a single IPK
*
* This utility replaces having to call `GroupDataProvider::SetKeySet` for the simple situation of a
* single IPK for a fabric, if a single epoch key is used. Start time will be set to 0 ("was always valid")
*
* @param provider - pointer to GroupDataProvider on which to set the IPK
* @param fabric_index - fabric index within the GroupDataProvider for which to set the IPK
* @param ipk_epoch_span - Span containing the IPK epoch key
* @param compressed_fabric_id - Compressed fabric ID associated with the fabric, for key derivation
* @return CHIP_NO_ERROR on success, CHIP_ERROR_INVALID_ARGUMENT on any bad argument, other CHIP_ERROR values
* from implementation on other errors
*/
inline CHIP_ERROR SetSingleIpkEpochKey(GroupDataProvider * provider, FabricIndex fabric_index, const ByteSpan & ipk_epoch_span,
const ByteSpan & compressed_fabric_id)
{
GroupDataProvider::KeySet ipkKeySet(GroupDataProvider::kIdentityProtectionKeySetId,
GroupDataProvider::SecurityPolicy::kTrustFirst, 1);
VerifyOrReturnError(provider != nullptr, CHIP_ERROR_INVALID_ARGUMENT);
VerifyOrReturnError(ipk_epoch_span.size() == sizeof(ipkKeySet.epoch_keys[0].key), CHIP_ERROR_INVALID_ARGUMENT);
VerifyOrReturnError(compressed_fabric_id.size() == sizeof(uint64_t), CHIP_ERROR_INVALID_ARGUMENT);
ipkKeySet.epoch_keys[0].start_time = 0;
memcpy(&ipkKeySet.epoch_keys[0].key, ipk_epoch_span.data(), ipk_epoch_span.size());
// Set a single IPK, validate key derivation follows spec
return provider->SetKeySet(fabric_index, compressed_fabric_id, ipkKeySet);
}
/**
* Instance getter for the global GroupDataProvider.
*
* Callers have to externally synchronize usage of this function.
*
* @return The global Group Data Provider
*/
GroupDataProvider * GetGroupDataProvider();
/**
* Instance setter for the global GroupDataProvider.
*
* Callers have to externally synchronize usage of this function.
*
* The `provider` can be set to nullptr if the owner is done with it fully.
*
* @param[in] provider pointer to the Group Data Provider global isntance to use
*/
void SetGroupDataProvider(GroupDataProvider * provider);
} // namespace Credentials
} // namespace chip