blob: 1f515f25565ca5ab5fdc321c5829b5169c1bec49 [file] [log] [blame]
/**
* Copyright (c) 2023 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.
*/
#include "MTRDeviceControllerDataStore.h"
// Importing MTRBaseDevice.h for the MTRAttributePath class. Needs to change when https://github.com/project-chip/connectedhomeip/issues/31247 is fixed.
#import "MTRBaseDevice.h"
#import "MTRLogging_Internal.h"
#include <lib/core/CASEAuthTag.h>
#include <lib/core/NodeId.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/SafeInt.h>
// FIXME: Are these good key strings? https://github.com/project-chip/connectedhomeip/issues/28973
static NSString * sResumptionNodeListKey = @"caseResumptionNodeList";
static NSString * sLastLocallyUsedNOCKey = @"lastLocallyUsedControllerNOC";
static NSString * ResumptionByNodeIDKey(NSNumber * nodeID)
{
return [NSString stringWithFormat:@"caseResumptionByNodeID/%llx", nodeID.unsignedLongLongValue];
}
static NSString * ResumptionByResumptionIDKey(NSData * resumptionID)
{
return
[NSString stringWithFormat:@"caseResumptionByResumptionID/%s", [resumptionID base64EncodedStringWithOptions:0].UTF8String];
}
static bool IsUnsignedIntegerNumber(id _Nullable value)
{
if (value == nil) {
return false;
}
if (![value isKindOfClass:[NSNumber class]]) {
return false;
}
NSNumber * number = value;
// Not sure how to check for the number being an integer.
if ([number compare:@(0)] == NSOrderedAscending) {
return false;
}
return true;
}
static bool IsValidNodeIDNumber(id _Nullable value)
{
// Node IDs cannot be negative.
if (!IsUnsignedIntegerNumber(value)) {
return false;
}
NSNumber * number = value;
// Validate that this is a valid operational ID, not some garbage unsigned
// int value that can't be a node id.
uint64_t unsignedValue = number.unsignedLongLongValue;
if (!chip::IsOperationalNodeId(unsignedValue)) {
return false;
}
return true;
}
static bool IsValidCATNumber(id _Nullable value)
{
// CATs cannot be negative.
if (!IsUnsignedIntegerNumber(value)) {
return false;
}
NSNumber * number = value;
// Validate that this is a valid CAT value and, not some garbage unsigned int
// value that can't be a CAT.
uint64_t unsignedValue = number.unsignedLongLongValue;
if (!chip::CanCastTo<chip::CASEAuthTag>(unsignedValue)) {
return false;
}
auto tag = static_cast<chip::CASEAuthTag>(unsignedValue);
if (!chip::IsValidCASEAuthTag(tag)) {
return false;
}
return true;
}
@implementation MTRDeviceControllerDataStore {
id<MTRDeviceControllerStorageDelegate> _storageDelegate;
dispatch_queue_t _storageDelegateQueue;
// Controller owns us, so we have to make sure to not keep it alive.
__weak MTRDeviceController * _controller;
// Array of nodes with resumption info, oldest-stored first.
NSMutableArray<NSNumber *> * _nodesWithResumptionInfo;
}
- (nullable instancetype)initWithController:(MTRDeviceController *)controller
storageDelegate:(id<MTRDeviceControllerStorageDelegate>)storageDelegate
storageDelegateQueue:(dispatch_queue_t)storageDelegateQueue
{
if (!(self = [super init])) {
return nil;
}
_controller = controller;
_storageDelegate = storageDelegate;
_storageDelegateQueue = storageDelegateQueue;
__block id resumptionNodeList;
dispatch_sync(_storageDelegateQueue, ^{
@autoreleasepool {
// NOTE: controller, not our weak ref, since we know it's still
// valid under this sync dispatch.
resumptionNodeList = [_storageDelegate controller:controller
valueForKey:sResumptionNodeListKey
securityLevel:MTRStorageSecurityLevelSecure
sharingType:MTRStorageSharingTypeNotShared];
}
});
if (resumptionNodeList != nil) {
if (![resumptionNodeList isKindOfClass:[NSArray class]]) {
MTR_LOG_ERROR("List of CASE resumption node IDs is not an array");
return nil;
}
for (id value in resumptionNodeList) {
if (!IsValidNodeIDNumber(value)) {
MTR_LOG_ERROR("Resumption node ID contains invalid value: %@", value);
return nil;
}
}
_nodesWithResumptionInfo = [resumptionNodeList mutableCopy];
} else {
_nodesWithResumptionInfo = [[NSMutableArray alloc] init];
}
return self;
}
- (void)fetchAttributeDataForAllDevices:(MTRDeviceControllerDataStoreClusterDataHandler)clusterDataHandler
{
__block NSDictionary<NSString *, id> * dataStoreSecureLocalValues = nil;
MTRDeviceController * controller = _controller;
VerifyOrReturn(controller != nil); // No way to call delegate without controller.
dispatch_sync(_storageDelegateQueue, ^{
if ([self->_storageDelegate respondsToSelector:@selector(valuesForController:securityLevel:sharingType:)]) {
dataStoreSecureLocalValues = [self->_storageDelegate valuesForController:controller securityLevel:MTRStorageSecurityLevelSecure sharingType:MTRStorageSharingTypeNotShared];
}
});
if (dataStoreSecureLocalValues.count) {
clusterDataHandler([self _getClusterDataFromSecureLocalValues:dataStoreSecureLocalValues]);
}
}
- (nullable MTRCASESessionResumptionInfo *)findResumptionInfoByNodeID:(NSNumber *)nodeID
{
return [self _findResumptionInfoWithKey:ResumptionByNodeIDKey(nodeID)];
}
- (nullable MTRCASESessionResumptionInfo *)findResumptionInfoByResumptionID:(NSData *)resumptionID
{
return [self _findResumptionInfoWithKey:ResumptionByResumptionIDKey(resumptionID)];
}
- (void)storeResumptionInfo:(MTRCASESessionResumptionInfo *)resumptionInfo
{
MTRDeviceController * controller = _controller;
VerifyOrReturn(controller != nil); // No way to call delegate without controller.
auto * oldInfo = [self findResumptionInfoByNodeID:resumptionInfo.nodeID];
dispatch_sync(_storageDelegateQueue, ^{
if (oldInfo != nil) {
// Remove old resumption id key. No need to do that for the
// node id, because we are about to overwrite it.
[_storageDelegate controller:controller
removeValueForKey:ResumptionByResumptionIDKey(oldInfo.resumptionID)
securityLevel:MTRStorageSecurityLevelSecure
sharingType:MTRStorageSharingTypeNotShared];
[_nodesWithResumptionInfo removeObject:resumptionInfo.nodeID];
}
[_storageDelegate controller:controller
storeValue:resumptionInfo
forKey:ResumptionByNodeIDKey(resumptionInfo.nodeID)
securityLevel:MTRStorageSecurityLevelSecure
sharingType:MTRStorageSharingTypeNotShared];
[_storageDelegate controller:controller
storeValue:resumptionInfo
forKey:ResumptionByResumptionIDKey(resumptionInfo.resumptionID)
securityLevel:MTRStorageSecurityLevelSecure
sharingType:MTRStorageSharingTypeNotShared];
// Update our resumption info node list.
[_nodesWithResumptionInfo addObject:resumptionInfo.nodeID];
[_storageDelegate controller:controller
storeValue:[_nodesWithResumptionInfo copy]
forKey:sResumptionNodeListKey
securityLevel:MTRStorageSecurityLevelSecure
sharingType:MTRStorageSharingTypeNotShared];
});
}
- (void)clearAllResumptionInfo
{
MTRDeviceController * controller = _controller;
VerifyOrReturn(controller != nil); // No way to call delegate without controller.
// Can we do less dispatch? We would need to have a version of
// _findResumptionInfoWithKey that assumes we are already on the right queue.
for (NSNumber * nodeID in _nodesWithResumptionInfo) {
auto * oldInfo = [self findResumptionInfoByNodeID:nodeID];
if (oldInfo != nil) {
dispatch_sync(_storageDelegateQueue, ^{
[_storageDelegate controller:controller
removeValueForKey:ResumptionByResumptionIDKey(oldInfo.resumptionID)
securityLevel:MTRStorageSecurityLevelSecure
sharingType:MTRStorageSharingTypeNotShared];
[_storageDelegate controller:controller
removeValueForKey:ResumptionByNodeIDKey(oldInfo.nodeID)
securityLevel:MTRStorageSecurityLevelSecure
sharingType:MTRStorageSharingTypeNotShared];
});
}
}
[_nodesWithResumptionInfo removeAllObjects];
}
- (CHIP_ERROR)storeLastLocallyUsedNOC:(MTRCertificateTLVBytes)noc
{
MTRDeviceController * controller = _controller;
VerifyOrReturnError(controller != nil, CHIP_ERROR_PERSISTED_STORAGE_FAILED); // No way to call delegate without controller.
__block BOOL ok;
dispatch_sync(_storageDelegateQueue, ^{
ok = [_storageDelegate controller:controller
storeValue:noc
forKey:sLastLocallyUsedNOCKey
securityLevel:MTRStorageSecurityLevelSecure
sharingType:MTRStorageSharingTypeNotShared];
});
return ok ? CHIP_NO_ERROR : CHIP_ERROR_PERSISTED_STORAGE_FAILED;
}
- (MTRCertificateTLVBytes _Nullable)fetchLastLocallyUsedNOC
{
MTRDeviceController * controller = _controller;
VerifyOrReturnValue(controller != nil, nil); // No way to call delegate without controller.
__block id data;
dispatch_sync(_storageDelegateQueue, ^{
@autoreleasepool {
data = [_storageDelegate controller:controller
valueForKey:sLastLocallyUsedNOCKey
securityLevel:MTRStorageSecurityLevelSecure
sharingType:MTRStorageSharingTypeNotShared];
}
});
if (data == nil) {
return nil;
}
if (![data isKindOfClass:[NSData class]]) {
return nil;
}
return data;
}
- (nullable MTRCASESessionResumptionInfo *)_findResumptionInfoWithKey:(nullable NSString *)key
{
MTRDeviceController * controller = _controller;
VerifyOrReturnValue(controller != nil, nil); // No way to call delegate without controller.
// key could be nil if [NSString stringWithFormat] returns nil for some reason.
if (key == nil) {
return nil;
}
__block id resumptionInfo;
dispatch_sync(_storageDelegateQueue, ^{
@autoreleasepool {
resumptionInfo = [_storageDelegate controller:controller
valueForKey:key
securityLevel:MTRStorageSecurityLevelSecure
sharingType:MTRStorageSharingTypeNotShared];
}
});
if (resumptionInfo == nil) {
return nil;
}
if (![resumptionInfo isKindOfClass:[MTRCASESessionResumptionInfo class]]) {
return nil;
}
return resumptionInfo;
}
#pragma - Attribute Cache utility
/** MTRDevice cache storage
*
* Per controller:
* NodeID index
* key: "attrCacheNodeIndex"
* value: list of nodeIDs
* EndpointID index
* key: "attrCacheEndpointIndex:<nodeID>"
* value: list of endpoint IDs
* ClusterID index
* key: "attrCacheClusterIndex:<nodeID>:<endpointID>"
* value: list of cluster IDs
* Cluster data entry:
* key: "attrCacheClusterData:<nodeID>:<endpointID>:<clusterID>"
* value: MTRDeviceClusterData
*/
- (id)_fetchAttributeCacheValueForKey:(NSString *)key expectedClass:(Class)expectedClass;
{
MTRDeviceController * controller = _controller;
VerifyOrReturnValue(controller != nil, nil); // No way to call delegate without controller.
id data;
@autoreleasepool {
data = [_storageDelegate controller:controller
valueForKey:key
securityLevel:MTRStorageSecurityLevelSecure
sharingType:MTRStorageSharingTypeNotShared];
}
if (data == nil) {
return nil;
}
if (![data isKindOfClass:expectedClass]) {
return nil;
}
return data;
}
- (BOOL)_storeAttributeCacheValue:(id)value forKey:(NSString *)key
{
MTRDeviceController * controller = _controller;
VerifyOrReturnValue(controller != nil, NO); // No way to call delegate without controller.
return [_storageDelegate controller:controller
storeValue:value
forKey:key
securityLevel:MTRStorageSecurityLevelSecure
sharingType:MTRStorageSharingTypeNotShared];
}
- (BOOL)_bulkStoreAttributeCacheValues:(NSDictionary<NSString *, id<NSSecureCoding>> *)values
{
MTRDeviceController * controller = _controller;
VerifyOrReturnValue(controller != nil, NO); // No way to call delegate without controller.
return [_storageDelegate controller:controller
storeValues:values
securityLevel:MTRStorageSecurityLevelSecure
sharingType:MTRStorageSharingTypeNotShared];
}
- (BOOL)_removeAttributeCacheValueForKey:(NSString *)key
{
MTRDeviceController * controller = _controller;
VerifyOrReturnValue(controller != nil, NO); // No way to call delegate without controller.
return [_storageDelegate controller:controller
removeValueForKey:key
securityLevel:MTRStorageSecurityLevelSecure
sharingType:MTRStorageSharingTypeNotShared];
}
static NSString * sAttributeCacheNodeIndexKey = @"attrCacheNodeIndex";
- (nullable NSArray<NSNumber *> *)_fetchNodeIndex
{
dispatch_assert_queue(_storageDelegateQueue);
return [self _fetchAttributeCacheValueForKey:sAttributeCacheNodeIndexKey expectedClass:[NSArray class]];
}
- (BOOL)_storeNodeIndex:(NSArray<NSNumber *> *)nodeIndex
{
dispatch_assert_queue(_storageDelegateQueue);
return [self _storeAttributeCacheValue:nodeIndex forKey:sAttributeCacheNodeIndexKey];
}
- (BOOL)_deleteNodeIndex
{
dispatch_assert_queue(_storageDelegateQueue);
return [self _removeAttributeCacheValueForKey:sAttributeCacheNodeIndexKey];
}
static NSString * sAttributeCacheEndpointIndexKeyPrefix = @"attrCacheEndpointIndex";
- (NSString *)_endpointIndexKeyForNodeID:(NSNumber *)nodeID
{
return [sAttributeCacheEndpointIndexKeyPrefix stringByAppendingFormat:@":0x%016llX", nodeID.unsignedLongLongValue];
}
- (nullable NSArray<NSNumber *> *)_fetchEndpointIndexForNodeID:(NSNumber *)nodeID
{
dispatch_assert_queue(_storageDelegateQueue);
if (!nodeID) {
MTR_LOG_ERROR("%s: unexpected nil input", __func__);
return nil;
}
return [self _fetchAttributeCacheValueForKey:[self _endpointIndexKeyForNodeID:nodeID] expectedClass:[NSArray class]];
}
- (BOOL)_storeEndpointIndex:(NSArray<NSNumber *> *)endpointIndex forNodeID:(NSNumber *)nodeID
{
dispatch_assert_queue(_storageDelegateQueue);
if (!nodeID) {
MTR_LOG_ERROR("%s: unexpected nil input", __func__);
return NO;
}
return [self _storeAttributeCacheValue:endpointIndex forKey:[self _endpointIndexKeyForNodeID:nodeID]];
}
- (BOOL)_deleteEndpointIndexForNodeID:(NSNumber *)nodeID
{
dispatch_assert_queue(_storageDelegateQueue);
if (!nodeID) {
MTR_LOG_ERROR("%s: unexpected nil input", __func__);
return NO;
}
return [self _removeAttributeCacheValueForKey:[self _endpointIndexKeyForNodeID:nodeID]];
}
static NSString * sAttributeCacheClusterIndexKeyPrefix = @"attrCacheClusterIndex";
- (NSString *)_clusterIndexKeyForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID
{
return [sAttributeCacheClusterIndexKeyPrefix stringByAppendingFormat:@":0x%016llX:%0x04X", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue];
}
- (nullable NSArray<NSNumber *> *)_fetchClusterIndexForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID
{
dispatch_assert_queue(_storageDelegateQueue);
if (!nodeID || !endpointID) {
MTR_LOG_ERROR("%s: unexpected nil input", __func__);
return nil;
}
return [self _fetchAttributeCacheValueForKey:[self _clusterIndexKeyForNodeID:nodeID endpointID:endpointID] expectedClass:[NSArray class]];
}
- (BOOL)_storeClusterIndex:(NSArray<NSNumber *> *)clusterIndex forNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID
{
dispatch_assert_queue(_storageDelegateQueue);
if (!nodeID || !endpointID) {
MTR_LOG_ERROR("%s: unexpected nil input", __func__);
return NO;
}
return [self _storeAttributeCacheValue:clusterIndex forKey:[self _clusterIndexKeyForNodeID:nodeID endpointID:endpointID]];
}
- (BOOL)_deleteClusterIndexForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID
{
dispatch_assert_queue(_storageDelegateQueue);
if (!nodeID || !endpointID) {
MTR_LOG_ERROR("%s: unexpected nil input", __func__);
return NO;
}
return [self _removeAttributeCacheValueForKey:[self _clusterIndexKeyForNodeID:nodeID endpointID:endpointID]];
}
static NSString * sAttributeCacheClusterDataKeyPrefix = @"attrCacheClusterData";
- (NSString *)_clusterDataKeyForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID
{
return [sAttributeCacheClusterDataKeyPrefix stringByAppendingFormat:@":0x%016llX:0x%04X:0x%08lX", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue, clusterID.unsignedLongValue];
}
- (nullable MTRDeviceClusterData *)_fetchClusterDataForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID
{
dispatch_assert_queue(_storageDelegateQueue);
if (!nodeID || !endpointID || !clusterID) {
MTR_LOG_ERROR("%s: unexpected nil input", __func__);
return nil;
}
return [self _fetchAttributeCacheValueForKey:[self _clusterDataKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID] expectedClass:[MTRDeviceClusterData class]];
}
- (BOOL)_storeClusterData:(MTRDeviceClusterData *)clusterData forNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID
{
dispatch_assert_queue(_storageDelegateQueue);
if (!nodeID || !endpointID || !clusterID || !clusterData) {
MTR_LOG_ERROR("%s: unexpected nil input", __func__);
return NO;
}
return [self _storeAttributeCacheValue:clusterData forKey:[self _clusterDataKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID]];
}
- (BOOL)_deleteClusterDataForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID
{
dispatch_assert_queue(_storageDelegateQueue);
if (!nodeID || !endpointID || !clusterID) {
MTR_LOG_ERROR("%s: unexpected nil input", __func__);
return NO;
}
return [self _removeAttributeCacheValueForKey:[self _clusterDataKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID]];
}
#pragma - Attribute Cache management
#ifndef ATTRIBUTE_CACHE_VERBOSE_LOGGING
#define ATTRIBUTE_CACHE_VERBOSE_LOGGING 0
#endif
#ifdef DEBUG
- (void)unitTestPruneEmptyStoredClusterDataBranches
{
dispatch_sync(_storageDelegateQueue, ^{
[self _pruneEmptyStoredClusterDataBranches];
});
}
#endif
- (void)_pruneEmptyStoredClusterDataBranches
{
dispatch_assert_queue(_storageDelegateQueue);
NSUInteger storeFailures = 0;
// Fetch node index
NSArray<NSNumber *> * nodeIndex = [self _fetchNodeIndex];
NSMutableArray<NSNumber *> * nodeIndexCopy = [nodeIndex mutableCopy];
for (NSNumber * nodeID in nodeIndex) {
// Fetch endpoint index
NSArray<NSNumber *> * endpointIndex = [self _fetchEndpointIndexForNodeID:nodeID];
NSMutableArray<NSNumber *> * endpointIndexCopy = [endpointIndex mutableCopy];
for (NSNumber * endpointID in endpointIndex) {
// Fetch cluster index
NSArray<NSNumber *> * clusterIndex = [self _fetchClusterIndexForNodeID:nodeID endpointID:endpointID];
NSMutableArray<NSNumber *> * clusterIndexCopy = [clusterIndex mutableCopy];
for (NSNumber * clusterID in clusterIndex) {
// Fetch cluster data, if it exists.
MTRDeviceClusterData * clusterData = [self _fetchClusterDataForNodeID:nodeID endpointID:endpointID clusterID:clusterID];
if (!clusterData) {
[clusterIndexCopy removeObject:clusterID];
}
}
if (clusterIndex.count != clusterIndexCopy.count) {
BOOL success;
if (clusterIndexCopy.count) {
success = [self _storeClusterIndex:clusterIndexCopy forNodeID:nodeID endpointID:endpointID];
} else {
[endpointIndexCopy removeObject:endpointID];
success = [self _deleteClusterIndexForNodeID:nodeID endpointID:endpointID];
}
if (!success) {
storeFailures++;
MTR_LOG_INFO("Store failed in _pruneEmptyStoredClusterDataBranches for clusterIndex (%lu) @ node 0x%016llX endpoint %u", static_cast<unsigned long>(clusterIndexCopy.count), nodeID.unsignedLongLongValue, endpointID.unsignedShortValue);
}
}
}
if (endpointIndex.count != endpointIndexCopy.count) {
BOOL success;
if (endpointIndexCopy.count) {
success = [self _storeEndpointIndex:endpointIndexCopy forNodeID:nodeID];
} else {
[nodeIndexCopy removeObject:nodeID];
success = [self _deleteEndpointIndexForNodeID:nodeID];
}
if (!success) {
storeFailures++;
MTR_LOG_INFO("Store failed in _pruneEmptyStoredClusterDataBranches for endpointIndex (%lu) @ node 0x%016llX", static_cast<unsigned long>(endpointIndexCopy.count), nodeID.unsignedLongLongValue);
}
}
}
if (nodeIndex.count != nodeIndexCopy.count) {
BOOL success;
if (nodeIndexCopy.count) {
success = [self _storeNodeIndex:nodeIndexCopy];
} else {
success = [self _deleteNodeIndex];
}
if (!success) {
storeFailures++;
MTR_LOG_INFO("Store failed in _pruneEmptyStoredClusterDataBranches for nodeIndex (%lu)", static_cast<unsigned long>(nodeIndexCopy.count));
}
}
if (storeFailures) {
MTR_LOG_ERROR("Store failed in _pruneEmptyStoredClusterDataBranches: failure count %lu", static_cast<unsigned long>(storeFailures));
}
}
- (void)_clearStoredClusterDataForNodeID:(NSNumber *)nodeID
{
dispatch_assert_queue(_storageDelegateQueue);
NSUInteger endpointsClearAttempts = 0;
NSUInteger clusterDataClearAttempts = 0;
NSUInteger endpointsCleared = 0;
NSUInteger clusterDataCleared = 0;
// Fetch endpoint index
NSArray<NSNumber *> * endpointIndex = [self _fetchEndpointIndexForNodeID:nodeID];
endpointsClearAttempts += endpointIndex.count;
for (NSNumber * endpointID in endpointIndex) {
// Fetch cluster index
NSArray<NSNumber *> * clusterIndex = [self _fetchClusterIndexForNodeID:nodeID endpointID:endpointID];
clusterDataClearAttempts += clusterIndex.count; // Assuming every cluster has cluster data
for (NSNumber * clusterID in clusterIndex) {
BOOL success = [self _deleteClusterDataForNodeID:nodeID endpointID:endpointID clusterID:clusterID];
if (!success) {
MTR_LOG_INFO("Delete failed for clusterData @ node 0x%016llX endpoint %u cluster 0x%08lX", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue, clusterID.unsignedLongValue);
} else {
clusterDataCleared++;
}
}
BOOL success = [self _deleteClusterIndexForNodeID:nodeID endpointID:endpointID];
if (!success) {
MTR_LOG_INFO("Delete failed for clusterIndex @ node 0x%016llX endpoint %u", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue);
} else {
endpointsCleared++;
}
}
BOOL success = [self _deleteEndpointIndexForNodeID:nodeID];
if (!success) {
MTR_LOG_INFO("Delete failed for endpointIndex @ node 0x%016llX", nodeID.unsignedLongLongValue);
}
MTR_LOG_INFO("clearStoredClusterDataForNodeID: deleted endpoints %lu/%lu clusters %lu/%lu", static_cast<unsigned long>(endpointsCleared), static_cast<unsigned long>(endpointsClearAttempts), static_cast<unsigned long>(clusterDataCleared), static_cast<unsigned long>(clusterDataClearAttempts));
}
- (void)clearStoredClusterDataForNodeID:(NSNumber *)nodeID
{
dispatch_async(_storageDelegateQueue, ^{
[self _clearStoredClusterDataForNodeID:nodeID];
NSArray<NSNumber *> * nodeIndex = [self _fetchNodeIndex];
NSMutableArray<NSNumber *> * nodeIndexCopy = [nodeIndex mutableCopy];
[nodeIndexCopy removeObject:nodeID];
if (nodeIndex.count != nodeIndexCopy.count) {
BOOL success;
if (nodeIndexCopy.count) {
success = [self _storeNodeIndex:nodeIndexCopy];
} else {
success = [self _deleteNodeIndex];
}
if (!success) {
MTR_LOG_INFO("Store failed in clearStoredAttributesForNodeID for nodeIndex (%lu)", static_cast<unsigned long>(nodeIndexCopy.count));
}
}
});
}
- (void)clearAllStoredClusterData
{
dispatch_async(_storageDelegateQueue, ^{
// Fetch node index
NSArray<NSNumber *> * nodeIndex = [self _fetchNodeIndex];
for (NSNumber * nodeID in nodeIndex) {
[self _clearStoredClusterDataForNodeID:nodeID];
}
BOOL success = [self _deleteNodeIndex];
if (!success) {
MTR_LOG_INFO("Delete failed for nodeIndex");
}
});
}
- (nullable NSDictionary<MTRClusterPath *, MTRDeviceClusterData *> *)getStoredClusterDataForNodeID:(NSNumber *)nodeID
{
if (!nodeID) {
MTR_LOG_ERROR("%s: unexpected nil input", __func__);
return nil;
}
__block NSMutableDictionary<MTRClusterPath *, MTRDeviceClusterData *> * clusterDataToReturn = nil;
dispatch_sync(_storageDelegateQueue, ^{
// Fetch node index
NSArray<NSNumber *> * nodeIndex = [self _fetchNodeIndex];
#if ATTRIBUTE_CACHE_VERBOSE_LOGGING
MTR_LOG_INFO("Fetch got %lu values for nodeIndex", static_cast<unsigned long>(nodeIndex.count));
#endif
if (![nodeIndex containsObject:nodeID]) {
// Sanity check and delete if nodeID exists in index
NSArray<NSNumber *> * endpointIndex = [self _fetchEndpointIndexForNodeID:nodeID];
if (endpointIndex) {
MTR_LOG_ERROR("Persistent attribute cache contains orphaned entry for nodeID %@ - deleting", nodeID);
// _clearStoredClusterDataForNodeID because we are are already
// on the _storageDelegateQueue and do not need to modify the
// node index in this case.
[self _clearStoredClusterDataForNodeID:nodeID];
}
MTR_LOG_INFO("Fetch got no value for endpointIndex @ node 0x%016llX", nodeID.unsignedLongLongValue);
clusterDataToReturn = nil;
return;
}
// Fetch endpoint index
NSArray<NSNumber *> * endpointIndex = [self _fetchEndpointIndexForNodeID:nodeID];
#if ATTRIBUTE_CACHE_VERBOSE_LOGGING
MTR_LOG_INFO("Fetch got %lu values for endpointIndex @ node 0x%016llX", static_cast<unsigned long>(endpointIndex.count), nodeID.unsignedLongLongValue);
#endif
for (NSNumber * endpointID in endpointIndex) {
// Fetch cluster index
NSArray<NSNumber *> * clusterIndex = [self _fetchClusterIndexForNodeID:nodeID endpointID:endpointID];
#if ATTRIBUTE_CACHE_VERBOSE_LOGGING
MTR_LOG_INFO("Fetch got %lu values for clusterIndex @ node 0x%016llX %u", static_cast<unsigned long>(clusterIndex.count), nodeID.unsignedLongLongValue, endpointID.unsignedShortValue);
#endif
for (NSNumber * clusterID in clusterIndex) {
// Fetch cluster data
MTRDeviceClusterData * clusterData = [self _fetchClusterDataForNodeID:nodeID endpointID:endpointID clusterID:clusterID];
if (!clusterData) {
MTR_LOG_INFO("Fetch got no value for clusterData @ node 0x%016llX endpoint %u cluster 0x%08lX", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue, clusterID.unsignedLongValue);
continue;
}
#if ATTRIBUTE_CACHE_VERBOSE_LOGGING
MTR_LOG_INFO("Fetch got clusterData @ node 0x%016llX endpoint %u cluster 0x%08lX", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue, clusterID.unsignedLongValue);
#endif
MTRClusterPath * path = [MTRClusterPath clusterPathWithEndpointID:endpointID clusterID:clusterID];
if (!clusterDataToReturn) {
clusterDataToReturn = [NSMutableDictionary dictionary];
}
clusterDataToReturn[path] = clusterData;
}
}
});
return clusterDataToReturn;
}
- (nullable MTRDeviceClusterData *)getStoredClusterDataForNodeID:(NSNumber *)nodeID endpointID:(NSNumber *)endpointID clusterID:(NSNumber *)clusterID
{
// Don't bother checking our indices here, since this will only be called
// when the consumer knows that we're supposed to have data for this cluster
// path.
__block MTRDeviceClusterData * clusterDataToReturn = nil;
dispatch_sync(_storageDelegateQueue, ^{
clusterDataToReturn = [self _fetchClusterDataForNodeID:nodeID endpointID:endpointID clusterID:clusterID];
});
return clusterDataToReturn;
}
// Utility for constructing dictionary of nodeID to cluster data from dictionary of storage keys
- (nullable NSDictionary<NSNumber *, NSDictionary<MTRClusterPath *, MTRDeviceClusterData *> *> *)_getClusterDataFromSecureLocalValues:(NSDictionary<NSString *, id> *)secureLocalValues
{
NSMutableDictionary<NSNumber *, NSDictionary<MTRClusterPath *, MTRDeviceClusterData *> *> * clusterDataByNodeToReturn = nil;
if (![secureLocalValues isKindOfClass:[NSDictionary class]]) {
return nil;
}
// Fetch node index
NSArray<NSNumber *> * nodeIndex = secureLocalValues[sAttributeCacheNodeIndexKey];
if (![nodeIndex isKindOfClass:[NSArray class]]) {
return nil;
}
for (NSNumber * nodeID in nodeIndex) {
if (![nodeID isKindOfClass:[NSNumber class]]) {
continue;
}
NSMutableDictionary<MTRClusterPath *, MTRDeviceClusterData *> * clusterDataForNode = nil;
NSArray<NSNumber *> * endpointIndex = secureLocalValues[[self _endpointIndexKeyForNodeID:nodeID]];
if (![endpointIndex isKindOfClass:[NSArray class]]) {
continue;
}
for (NSNumber * endpointID in endpointIndex) {
if (![endpointID isKindOfClass:[NSNumber class]]) {
continue;
}
NSArray<NSNumber *> * clusterIndex = secureLocalValues[[self _clusterIndexKeyForNodeID:nodeID endpointID:endpointID]];
if (![clusterIndex isKindOfClass:[NSArray class]]) {
continue;
}
for (NSNumber * clusterID in clusterIndex) {
if (![clusterID isKindOfClass:[NSNumber class]]) {
continue;
}
MTRDeviceClusterData * clusterData = secureLocalValues[[self _clusterDataKeyForNodeID:nodeID endpointID:endpointID clusterID:clusterID]];
if (!clusterData) {
continue;
}
if (![clusterData isKindOfClass:[MTRDeviceClusterData class]]) {
continue;
}
MTRClusterPath * clusterPath = [MTRClusterPath clusterPathWithEndpointID:endpointID clusterID:clusterID];
if (!clusterDataForNode) {
clusterDataForNode = [NSMutableDictionary dictionary];
}
clusterDataForNode[clusterPath] = clusterData;
}
}
if (clusterDataForNode.count) {
if (!clusterDataByNodeToReturn) {
clusterDataByNodeToReturn = [NSMutableDictionary dictionary];
}
clusterDataByNodeToReturn[nodeID] = clusterDataForNode;
}
}
return clusterDataByNodeToReturn;
}
- (void)storeClusterData:(NSDictionary<MTRClusterPath *, MTRDeviceClusterData *> *)clusterData forNodeID:(NSNumber *)nodeID
{
if (!nodeID) {
MTR_LOG_ERROR("%s: unexpected nil input", __func__);
return;
}
if (!clusterData.count) {
MTR_LOG_ERROR("%s: nothing to store", __func__);
return;
}
dispatch_async(_storageDelegateQueue, ^{
NSUInteger storeFailures = 0;
NSMutableDictionary<NSString *, id<NSSecureCoding>> * bulkValuesToStore = nil;
if ([self->_storageDelegate respondsToSelector:@selector(controller:storeValues:securityLevel:sharingType:)]) {
bulkValuesToStore = [NSMutableDictionary dictionary];
}
// A map of endpoint => list of clusters modified for that endpoint so cluster indexes can be updated later
NSMutableDictionary<NSNumber *, NSMutableSet<NSNumber *> *> * clustersModified = [NSMutableDictionary dictionary];
// Write cluster specific data first
for (MTRClusterPath * path in clusterData) {
MTRDeviceClusterData * data = clusterData[path];
#if ATTRIBUTE_CACHE_VERBOSE_LOGGING
MTR_LOG_INFO("Attempt to store clusterData @ node 0x%016llX endpoint %u cluster 0x%08lX", nodeID.unsignedLongLongValue, path.endpoint.unsignedShortValue, path.cluster.unsignedLongValue);
#endif
if (bulkValuesToStore) {
bulkValuesToStore[[self _clusterDataKeyForNodeID:nodeID endpointID:path.endpoint clusterID:path.cluster]] = data;
} else {
// Store cluster data
BOOL storeFailed = ![self _storeClusterData:data forNodeID:nodeID endpointID:path.endpoint clusterID:path.cluster];
if (storeFailed) {
storeFailures++;
MTR_LOG_INFO("Store failed for clusterData @ node 0x%016llX endpoint %u cluster 0x%08lX", nodeID.unsignedLongLongValue, path.endpoint.unsignedShortValue, path.cluster.unsignedLongValue);
}
}
// Note the cluster as modified for the endpoint
NSMutableSet<NSNumber *> * clustersInEndpoint = clustersModified[path.endpoint];
if (!clustersInEndpoint) {
clustersInEndpoint = [NSMutableSet set];
clustersModified[path.endpoint] = clustersInEndpoint;
}
[clustersInEndpoint addObject:path.cluster];
}
// Prepare to tally all endpoints touched
NSArray<NSNumber *> * endpointIndex = [self _fetchEndpointIndexForNodeID:nodeID];
BOOL endpointIndexModified = NO;
NSMutableArray<NSNumber *> * endpointIndexToStore;
if (endpointIndex) {
endpointIndexToStore = [endpointIndex mutableCopy];
} else {
endpointIndexToStore = [NSMutableArray array];
endpointIndexModified = YES;
}
for (NSNumber * endpointID in clustersModified) {
if (![endpointIndexToStore containsObject:endpointID]) {
[endpointIndexToStore addObject:endpointID];
endpointIndexModified = YES;
}
// Get cluster index from storage and prepare to tally clusters touched
NSSet * newClusters = clustersModified[endpointID];
NSArray<NSNumber *> * clusterIndex = [self _fetchClusterIndexForNodeID:nodeID endpointID:endpointID];
BOOL clusterIndexModified = NO;
NSMutableArray<NSNumber *> * clusterIndexToStore;
if (clusterIndex) {
clusterIndexToStore = [clusterIndex mutableCopy];
} else {
clusterIndexToStore = [NSMutableArray array];
clusterIndexModified = YES;
}
for (NSNumber * clusterID in newClusters) {
if (![clusterIndexToStore containsObject:clusterID]) {
[clusterIndexToStore addObject:clusterID];
clusterIndexModified = YES;
}
}
if (clusterIndexModified) {
if (bulkValuesToStore) {
bulkValuesToStore[[self _clusterIndexKeyForNodeID:nodeID endpointID:endpointID]] = clusterIndexToStore;
} else {
BOOL storeFailed = ![self _storeClusterIndex:clusterIndexToStore forNodeID:nodeID endpointID:endpointID];
if (storeFailed) {
storeFailures++;
MTR_LOG_INFO("Store failed for clusterIndex @ node 0x%016llX endpoint %u", nodeID.unsignedLongLongValue, endpointID.unsignedShortValue);
continue;
}
}
}
}
// Update endpoint index as needed
if (endpointIndexModified) {
if (bulkValuesToStore) {
bulkValuesToStore[[self _endpointIndexKeyForNodeID:nodeID]] = endpointIndexToStore;
} else {
BOOL storeFailed = ![self _storeEndpointIndex:endpointIndexToStore forNodeID:nodeID];
if (storeFailed) {
storeFailures++;
MTR_LOG_INFO("Store failed for endpointIndex @ node 0x%016llX", nodeID.unsignedLongLongValue);
}
}
}
// Check if node index needs updating / creation
NSArray<NSNumber *> * nodeIndex = [self _fetchNodeIndex];
NSArray<NSNumber *> * nodeIndexToStore = nil;
if (!nodeIndex) {
// Ensure node index exists
nodeIndexToStore = [NSArray arrayWithObject:nodeID];
} else if (![nodeIndex containsObject:nodeID]) {
nodeIndexToStore = [nodeIndex arrayByAddingObject:nodeID];
}
if (nodeIndexToStore) {
if (bulkValuesToStore) {
bulkValuesToStore[sAttributeCacheNodeIndexKey] = nodeIndexToStore;
} else {
BOOL storeFailed = ![self _storeNodeIndex:nodeIndexToStore];
if (storeFailed) {
storeFailures++;
MTR_LOG_INFO("Store failed for nodeIndex");
}
}
}
if (bulkValuesToStore) {
BOOL storeFailed = ![self _bulkStoreAttributeCacheValues:bulkValuesToStore];
if (storeFailed) {
storeFailures++;
MTR_LOG_INFO("Store failed for bulk values count %lu", static_cast<unsigned long>(bulkValuesToStore.count));
}
}
// In the rare event that store fails, allow all cluster data store attempts to go through and prune empty branches at the end altogether.
if (storeFailures) {
[self _pruneEmptyStoredClusterDataBranches];
MTR_LOG_ERROR("Store failed in -storeClusterData:forNodeID: failure count %lu", static_cast<unsigned long>(storeFailures));
}
});
}
@end
@implementation MTRCASESessionResumptionInfo
#pragma mark - NSSecureCoding
static NSString * const sNodeIDKey = @"nodeID";
static NSString * const sResumptionIDKey = @"resumptionID";
static NSString * const sSharedSecretKey = @"sharedSecret";
static NSString * const sCATsKey = @"CATs";
+ (BOOL)supportsSecureCoding
{
return YES;
}
- (nullable instancetype)initWithCoder:(NSCoder *)decoder
{
self = [super init];
if (self == nil) {
return nil;
}
_nodeID = [decoder decodeObjectOfClass:[NSNumber class] forKey:sNodeIDKey];
// For some built-in classes decoder will decode to them even if we ask for a
// different class (!). So sanity-check what we got.
if (_nodeID != nil && ![_nodeID isKindOfClass:[NSNumber class]]) {
MTR_LOG_ERROR("MTRCASESessionResumptionInfo got %@ for node ID, not NSNumber.", _nodeID);
return nil;
}
if (!IsValidNodeIDNumber(_nodeID)) {
MTR_LOG_ERROR("MTRCASESessionResumptionInfo node ID has invalid value: %@", _nodeID);
return nil;
}
_resumptionID = [decoder decodeObjectOfClass:[NSData class] forKey:sResumptionIDKey];
if (_resumptionID != nil && ![_resumptionID isKindOfClass:[NSData class]]) {
MTR_LOG_ERROR("MTRCASESessionResumptionInfo got %@ for resumption ID, not NSData.", _resumptionID);
return nil;
}
_sharedSecret = [decoder decodeObjectOfClass:[NSData class] forKey:sSharedSecretKey];
if (_sharedSecret != nil && ![_sharedSecret isKindOfClass:[NSData class]]) {
MTR_LOG_ERROR("MTRCASESessionResumptionInfo got %@ for shared secret, not NSData.", _sharedSecret);
return nil;
}
auto caseAuthenticatedTagArray = [decoder decodeArrayOfObjectsOfClass:[NSNumber class] forKey:sCATsKey];
for (id value in caseAuthenticatedTagArray) {
if (!IsValidCATNumber(value)) {
MTR_LOG_ERROR("MTRCASESessionResumptionInfo CASE tag has invalid value: %@", value);
return nil;
}
// Range-checking will be done when we try to convert the set to CATValues.
}
_caseAuthenticatedTags = [NSSet setWithArray:caseAuthenticatedTagArray];
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:self.nodeID forKey:sNodeIDKey];
[coder encodeObject:self.resumptionID forKey:sResumptionIDKey];
[coder encodeObject:self.sharedSecret forKey:sSharedSecretKey];
// Encode the CATs as an array, so that we can decodeArrayOfObjectsOfClass
// to get type-safe decoding for them.
[coder encodeObject:[self.caseAuthenticatedTags allObjects] forKey:sCATsKey];
}
@end
NSSet<Class> * MTRDeviceControllerStorageClasses()
{
// This only needs to return the classes for toplevel things we are storing,
// plus NSNumber because some archivers use that internally to store
// information about what's being archived.
static NSSet * const sStorageClasses = [NSSet setWithArray:@[
[NSNumber class],
[NSData class],
[NSArray class],
[MTRCASESessionResumptionInfo class],
[NSDictionary class],
[NSString class],
[MTRAttributePath class],
[MTRDeviceClusterData class],
]];
return sStorageClasses;
}