diff --git a/src/darwin/Framework/CHIP/MTRDemuxingStorage.h b/src/darwin/Framework/CHIP/MTRDemuxingStorage.h new file mode 100644 index 00000000000000..bfff6369800bc7 --- /dev/null +++ b/src/darwin/Framework/CHIP/MTRDemuxingStorage.h @@ -0,0 +1,70 @@ +/** + * 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. + */ + +#import +#import + +#include +#include + +NS_ASSUME_NONNULL_BEGIN + +/** + * A PersistentStorageDelegate implementation that does the following: + * + * 1) Ensures that any "global" storage keys are stored in RAM as needed so that + * the Matter stack has access to them. + * 2) Hands off fabric-index-specific keys to the controller that corresponds to + * that fabric index, if any. + */ +class MTRDemuxingStorage : public chip::PersistentStorageDelegate { +public: + MTRDemuxingStorage(MTRDeviceControllerFactory * factory); + ~MTRDemuxingStorage() {} + + // PersistentStorageDelegate API. + CHIP_ERROR SyncGetKeyValue(const char * key, void * buffer, uint16_t & size) override; + + CHIP_ERROR SyncSetKeyValue(const char * key, const void * value, uint16_t size) override; + + CHIP_ERROR SyncDeleteKeyValue(const char * key) override; + +private: + /** + * Methods for reading/writing/deleting things. The index-specific ones + * will have the "f/index/" bit already stripped of from the front of the key. + */ + NSData * _Nullable GetGlobalValue(NSString * key); + NSData * _Nullable GetIndexSpecificValue(chip::FabricIndex index, NSString * key); + + CHIP_ERROR SetGlobalValue(NSString * key, NSData * data); + CHIP_ERROR SetIndexSpecificValue(chip::FabricIndex index, NSString * key, NSData * data); + + CHIP_ERROR DeleteGlobalValue(NSString * key); + CHIP_ERROR DeleteIndexSpecificValue(chip::FabricIndex index, NSString * key); + + /** + * Methods for modifying our in-memory store for fully qualified keys. + */ + NSData * _Nullable GetInMemoryValue(NSString * key); + CHIP_ERROR SetInMemoryValue(NSString * key, NSData * data); + CHIP_ERROR DeleteInMemoryValue(NSString * key); + + MTRDeviceControllerFactory * mFactory; + NSMutableDictionary * mInMemoryStore; +}; + +NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIP/MTRDemuxingStorage.mm b/src/darwin/Framework/CHIP/MTRDemuxingStorage.mm new file mode 100644 index 00000000000000..5d4162ef321c55 --- /dev/null +++ b/src/darwin/Framework/CHIP/MTRDemuxingStorage.mm @@ -0,0 +1,416 @@ +/** + * 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. + */ + +#import "MTRDemuxingStorage.h" + +#import "MTRDeviceControllerFactory_Internal.h" +#import "MTRDeviceController_Internal.h" +#import "MTRLogging_Internal.h" + +#include +#include +#include + +using namespace chip; + +static bool IsGlobalKey(NSString * key) { return [key hasPrefix:@"g/"]; } + +/** + * Checks for a key that is scoped to a specific fabric index. + */ +static bool IsIndexSpecificKey(NSString * key) { return [key hasPrefix:@"f/"]; } + +/** + * Extracts the fabric index from an index-specific key. Fails if the key + * is not index-specific or if a numeric FabricIndex could not be extracted + * from it. + */ +static CHIP_ERROR ExtractIndexFromKey(NSString * key, FabricIndex * index) +{ + if (!IsIndexSpecificKey(key)) { + return CHIP_ERROR_INVALID_ARGUMENT; + } + + auto * components = [key componentsSeparatedByString:@"/"]; + if (components.count < 3) { + // Unexpected "f/something" without any actual data. + return CHIP_ERROR_INVALID_ARGUMENT; + } + + auto * indexString = components[1]; + auto * scanner = [NSScanner scannerWithString:indexString]; + + auto * charset = [NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdefABCDEF"]; + charset = [charset invertedSet]; + + if ([scanner scanCharactersFromSet:charset intoString:nil] == YES) { + // Leading non-hex chars. + return CHIP_ERROR_INVALID_ARGUMENT; + } + + unsigned int value; + if ([scanner scanHexInt:&value] == NO) { + return CHIP_ERROR_INVALID_ARGUMENT; + } + + if (scanner.atEnd == NO) { + // Trailing garbage chars. + return CHIP_ERROR_INVALID_ARGUMENT; + } + + if (!CanCastTo(value)) { + return CHIP_ERROR_INVALID_ARGUMENT; + } + + *index = static_cast(value); + return CHIP_NO_ERROR; +} + +/** + * Extracts the "index-specific" part of an index-specific key (i.e. the + * part after "f/index/"). + */ +static CHIP_ERROR ExtractIndexSpecificKey(NSString * key, NSString * __autoreleasing * extractedKey) +{ + if (!IsIndexSpecificKey(key)) { + return CHIP_ERROR_INVALID_ARGUMENT; + } + + auto * components = [key componentsSeparatedByString:@"/"]; + if (components.count < 3) { + // Unexpected "f/something" without any actual data. + return CHIP_ERROR_INVALID_ARGUMENT; + } + + components = [components subarrayWithRange:NSMakeRange(2, components.count - 2)]; + *extractedKey = [components componentsJoinedByString:@"/"]; + return CHIP_NO_ERROR; +} + +/** + * Method to test whether a global key should be stored in memory only, as + * opposed to being passed on to the actual storage related to controllers. + */ +static bool IsMemoryOnlyGlobalKey(NSString * key) +{ + if ([key isEqualToString:@"g/fidx"]) { + // Fabric index list only needs to be stored in-memory, not persisted, + // because we do not tie controllers to specific fabric indices. + return true; + } + + if ([key isEqualToString:@"g/fs/c"] || [key isEqualToString:@"g/fs/n"]) { + // Just store the fail-safe markers in memory as well. We could + // plausibly not store them at all, since we never actually need to + // clean up anything by fabric index, but this is safer in case + // consumers try to read back right after writing. + return true; + } + + if ([key isEqualToString:@"g/lkgt"]) { + // Store Last Known Good Time in memory only. We never need this in + // general, because we can always provide the wall-clock time. + return true; + } + + // We do not expect to see the "g/sri" or "g/s/*" keys for session + // resumption, because we implement SessionResumptionStorage ourselves. + + // For now, put group global counters in memory. + // TODO: we should inject a group counter manager that makes these counters + // per-controller, not globally handled via storage. + // See https://github.com/project-chip/connectedhomeip/issues/28510 + if ([key isEqualToString:@"g/gdc"] || [key isEqualToString:@"g/gcc"]) { + return true; + } + + // We do not expect to see "g/userlbl/*" User Label keys. + + // We do not expect to see the "g/gfl" key for endpoint-to-group + // associations. + + // We do not expect to see the "g/a/*" keys for attribute values. + + // We do not expect to see the "g/bt" and "g/bt/*" keys for the binding + // table. + + // We do not expect to see the "g/o/*" OTA Requestor keys. + + // We do not expect to see the "g/im/ec" event number counter key. + + // We do not expect to see the "g/su/*" and "g/sum" keys for server-side + // subscription resumption storage. + + // We do not expect to see the "g/scc/*" scenes keys. + + // We do not expect to see the "g/ts/tts", "g/ts/dntp", "g/ts/tz", + // "g/ts/dsto" Time Synchronization keys. + + return false; +} + +/** + * Method to test whether an index-specific key should be stored in memory only, as + * opposed to being passed on to the actual storage related to controllers. + * The key string will have the "f/index/" bit already stripped off the + * front of the key. + */ +static bool IsMemoryOnlyIndexSpecificKey(NSString * key) +{ + // Store all the fabric table bits in memory only. This is done because the + // fabric table expects none of these things to be stored in the case of a + // "new fabric addition", which is what we always do when using + // per-controller storage. + // + // TODO: Figure out which, if any, of these things we should also store for + // later recall when starting a controller with storage we have used before. + // + // For future reference: + // + // n == NOC + // i == ICAC + // r == RCAC + // m == Fabric metadata (TLV containing the vendor ID) + // o == operational key, only written if internally generated. + if ([key isEqualToString:@"n"] || [key isEqualToString:@"i"] || [key isEqualToString:@"r"] || [key isEqualToString:@"m"] || + [key isEqualToString:@"o"]) { + return true; + } + + // We do not expect to see the "s/*" keys for session resumption, because we + // implement SessionResumptionStorage ourselves. + + // We do not expect to see the "ac/*" keys for ACL entries. + + // We do not expect to see the "g" or "g/*" keys for which endpoints are in which + // group. + + // For now, just store group keysets and group keys in memory. + // TODO: We want to start persisting these, per-controller, if we're going + // to support group keys. Or inject a GroupDataProvider of our own instead + // of using Credentials::GroupDataProviderImp and then + // not be tied to whatever storage format that uses. + // https://github.com/project-chip/connectedhomeip/issues/28511 + if ([key hasPrefix:@"gk/"] || [key hasPrefix:@"k/"]) { + return true; + } + + // We do not expect to see the "icd/*" keys for the ICD Management table. + + // We do not expect to see the "e/*" scenes keys. + + return false; +} + +/** + * Method to convert an index-specific key into a fully qualified key. + */ +static NSString * FullyQualifiedKey(FabricIndex index, NSString * key) +{ + return [NSString stringWithFormat:@"f/%x/%s", index, key.UTF8String]; +} + +MTRDemuxingStorage::MTRDemuxingStorage(MTRDeviceControllerFactory * factory) + : mFactory(factory) +{ + mInMemoryStore = [[NSMutableDictionary alloc] init]; +} + +CHIP_ERROR MTRDemuxingStorage::SyncGetKeyValue(const char * key, void * buffer, uint16_t & size) +{ + assertChipStackLockedByCurrentThread(); + if (buffer == nullptr && size != 0) { + return CHIP_ERROR_INVALID_ARGUMENT; + } + + NSString * keyString = [NSString stringWithUTF8String:key]; + +#if LOG_DEBUG_PERSISTENT_STORAGE_DELEGATE + MTR_LOG_DEBUG("MTRDemuxingStorage Sync Get Value for Key: %@", keyString); +#endif + + NSData * value; + if (IsGlobalKey(keyString)) { + value = GetGlobalValue(keyString); + } else if (IsIndexSpecificKey(keyString)) { + FabricIndex index; + ReturnErrorOnFailure(ExtractIndexFromKey(keyString, &index)); + ReturnErrorOnFailure(ExtractIndexSpecificKey(keyString, &keyString)); + value = GetIndexSpecificValue(index, keyString); + } else { + return CHIP_ERROR_INVALID_ARGUMENT; + } + + if (value == nil) { + return CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND; + } + + if (value.length > UINT16_MAX) { + return CHIP_ERROR_PERSISTED_STORAGE_FAILED; + } + + uint16_t valueSize = static_cast(value.length); + if (valueSize > size) { + return CHIP_ERROR_BUFFER_TOO_SMALL; + } + + size = valueSize; + + if (size != 0) { + // buffer is known to be non-null here. + memcpy(buffer, value.bytes, size); + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR MTRDemuxingStorage::SyncSetKeyValue(const char * key, const void * value, uint16_t size) +{ + if (value == nullptr && size != 0) { + return CHIP_ERROR_INVALID_ARGUMENT; + } + + NSString * keyString = [NSString stringWithUTF8String:key]; + NSData * valueData = (value == nullptr) ? [NSData data] : [NSData dataWithBytes:value length:size]; + +#if LOG_DEBUG_PERSISTENT_STORAGE_DELEGATE + MTR_LOG_DEBUG("MTRDemuxingStorage Set Key %@", keyString); +#endif + + if (IsGlobalKey(keyString)) { + return SetGlobalValue(keyString, valueData); + } + + if (IsIndexSpecificKey(keyString)) { + FabricIndex index; + ReturnErrorOnFailure(ExtractIndexFromKey(keyString, &index)); + ReturnErrorOnFailure(ExtractIndexSpecificKey(keyString, &keyString)); + return SetIndexSpecificValue(index, keyString, valueData); + } + + return CHIP_ERROR_INVALID_ARGUMENT; +} + +CHIP_ERROR MTRDemuxingStorage::SyncDeleteKeyValue(const char * key) +{ + NSString * keyString = [NSString stringWithUTF8String:key]; + +#if LOG_DEBUG_PERSISTENT_STORAGE_DELEGATE + MTR_LOG_DEBUG("MTRDemuxingStorage Delete Key: %@", keyString); +#endif + + if (IsGlobalKey(keyString)) { + return DeleteGlobalValue(keyString); + } + + if (IsIndexSpecificKey(keyString)) { + FabricIndex index; + ReturnErrorOnFailure(ExtractIndexFromKey(keyString, &index)); + ReturnErrorOnFailure(ExtractIndexSpecificKey(keyString, &keyString)); + return DeleteIndexSpecificValue(index, keyString); + } + + return CHIP_ERROR_INVALID_ARGUMENT; +} + +NSData * _Nullable MTRDemuxingStorage::GetGlobalValue(NSString * key) +{ + if (IsMemoryOnlyGlobalKey(key)) { + return GetInMemoryValue(key); + } + + MTR_LOG_ERROR("MTRDemuxingStorage reading unknown global key: %@", key); + + return nil; +} + +NSData * _Nullable MTRDemuxingStorage::GetIndexSpecificValue(FabricIndex index, NSString * key) +{ + if (IsMemoryOnlyIndexSpecificKey(key)) { + return GetInMemoryValue(FullyQualifiedKey(index, key)); + } + + return nil; +} + +CHIP_ERROR MTRDemuxingStorage::SetGlobalValue(NSString * key, NSData * data) +{ + if (IsMemoryOnlyGlobalKey(key)) { + // Fabric index list only needs to be stored in-memory, not persisted, + // because we do not tie controllers to specific fabric indices. + return SetInMemoryValue(key, data); + } + + MTR_LOG_ERROR("MTRDemuxingStorage setting unknown global key: %@", key); + + return CHIP_ERROR_PERSISTED_STORAGE_FAILED; +} + +CHIP_ERROR MTRDemuxingStorage::SetIndexSpecificValue(FabricIndex index, NSString * key, NSData * data) +{ + if ([key isEqualToString:@"n"]) { + // Index-scoped "n" is NOC. + auto * controller = [mFactory runningControllerForFabricIndex:index]; + if (controller == nil) { + return CHIP_ERROR_PERSISTED_STORAGE_FAILED; + } + + ReturnErrorOnFailure([controller.controllerDataStore storeLastLocallyUsedNOC:data]); + } + + if (IsMemoryOnlyIndexSpecificKey(key)) { + return SetInMemoryValue(FullyQualifiedKey(index, key), data); + } + + return CHIP_ERROR_PERSISTED_STORAGE_FAILED; +} + +CHIP_ERROR MTRDemuxingStorage::DeleteGlobalValue(NSString * key) +{ + if (IsMemoryOnlyGlobalKey(key)) { + return DeleteInMemoryValue(key); + } + + MTR_LOG_ERROR("MTRDemuxingStorage deleting unknown global key: %@", key); + + return CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND; +} + +CHIP_ERROR MTRDemuxingStorage::DeleteIndexSpecificValue(FabricIndex index, NSString * key) +{ + if (IsMemoryOnlyIndexSpecificKey(key)) { + return DeleteInMemoryValue(FullyQualifiedKey(index, key)); + } + + return CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND; +} + +NSData * _Nullable MTRDemuxingStorage::GetInMemoryValue(NSString * key) { return mInMemoryStore[key]; } + +CHIP_ERROR MTRDemuxingStorage::SetInMemoryValue(NSString * key, NSData * data) +{ + mInMemoryStore[key] = data; + return CHIP_NO_ERROR; +} + +CHIP_ERROR MTRDemuxingStorage::DeleteInMemoryValue(NSString * key) +{ + BOOL present = (mInMemoryStore[key] != nil); + if (present) { + [mInMemoryStore removeObjectForKey:key]; + } + return present ? CHIP_NO_ERROR : CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND; +} diff --git a/src/darwin/Framework/CHIP/MTRDeviceController.h b/src/darwin/Framework/CHIP/MTRDeviceController.h index fce1a88002aaf8..670e12bbd0caaf 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceController.h +++ b/src/darwin/Framework/CHIP/MTRDeviceController.h @@ -47,6 +47,11 @@ typedef void (^MTRDeviceConnectionCallback)(MTRBaseDevice * _Nullable device, NS */ @property (readonly, nonatomic, getter=isRunning) BOOL running; +/** + * The ID assigned to this controller at creation time. + */ +@property (readonly, nonatomic) NSUUID * uniqueIdentifier MTR_NEWLY_AVAILABLE; + /** * Return the Node ID assigned to the controller. Will return nil if the * controller is not running (and hence does not know its node id). diff --git a/src/darwin/Framework/CHIP/MTRDeviceController.mm b/src/darwin/Framework/CHIP/MTRDeviceController.mm index 4b2c32d9c2bf0d..403bc8a0906183 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceController.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceController.mm @@ -114,9 +114,30 @@ @interface MTRDeviceController () { @implementation MTRDeviceController -- (instancetype)initWithFactory:(MTRDeviceControllerFactory *)factory queue:(dispatch_queue_t)queue +- (instancetype)initWithFactory:(MTRDeviceControllerFactory *)factory + queue:(dispatch_queue_t)queue + storageDelegate:(id _Nullable)storageDelegate + storageDelegateQueue:(dispatch_queue_t _Nullable)storageDelegateQueue + uniqueIdentifier:(NSUUID *)uniqueIdentifier { if (self = [super init]) { + // Make sure our storage is all set up to work as early as possible, + // before we start doing anything else with the controller. + _uniqueIdentifier = uniqueIdentifier; + if (storageDelegate != nil) { + if (storageDelegateQueue == nil) { + MTR_LOG_ERROR("storageDelegate provided without storageDelegateQueue"); + return nil; + } + + _controllerDataStore = [[MTRDeviceControllerDataStore alloc] initWithController:self + storageDelegate:storageDelegate + storageDelegateQueue:storageDelegateQueue]; + if (_controllerDataStore == nil) { + return nil; + } + } + _chipWorkQueue = queue; _factory = factory; _deviceMapLock = OS_UNFAIR_LOCK_INIT; @@ -191,8 +212,11 @@ - (void)shutDownCppController // _cppCommissioner, so we're not in a state where we claim to be // running but are actually partially shut down. _cppCommissioner = nullptr; - _storedFabricIndex = chip::kUndefinedFabricIndex; commissionerToShutDown->Shutdown(); + // Don't clear out our fabric index association until controller + // shutdown completes, in case it wants to write to storage as it + // shuts down. + _storedFabricIndex = chip::kUndefinedFabricIndex; delete commissionerToShutDown; if (_operationalCredentialsDelegate != nil) { _operationalCredentialsDelegate->SetDeviceCommissioner(nullptr); @@ -365,6 +389,7 @@ - (BOOL)startup:(MTRDeviceControllerStartupParamsInternal *)startupParams // bring-up. commissionerParams.removeFromFabricTableOnShutdown = false; commissionerParams.deviceAttestationVerifier = _factory.deviceAttestationVerifier; + commissionerParams.permitMultiControllerFabrics = startupParams.allowMultipleControllersPerFabric; auto & factory = chip::Controller::DeviceControllerFactory::GetInstance(); diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.h b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.h new file mode 100644 index 00000000000000..8d10c7af3e810b --- /dev/null +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.h @@ -0,0 +1,66 @@ +/** + * 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. + */ + +#import +#import +#import +#import + +#include + +NS_ASSUME_NONNULL_BEGIN + +/** + * Interface that represents a single CASE session resumption entry. + */ +MTR_HIDDEN +@interface MTRCASESessionResumptionInfo : NSObject +@property (nonatomic) NSNumber * nodeID; +@property (nonatomic) NSData * resumptionID; +@property (nonatomic) NSData * sharedSecret; +@property (nonatomic) NSSet * caseAuthenticatedTags; +@end + +/** + * Interface that wraps a type-safe API around + * MTRDeviceControllerStorageDelegate. + */ +MTR_HIDDEN +@interface MTRDeviceControllerDataStore : NSObject + +- (nullable instancetype)initWithController:(MTRDeviceController *)controller + storageDelegate:(id)storageDelegate + storageDelegateQueue:(dispatch_queue_t)storageDelegateQueue; + +/** + * Resumption info APIs. + */ +- (nullable MTRCASESessionResumptionInfo *)findResumptionInfoByNodeID:(NSNumber *)nodeID; +- (nullable MTRCASESessionResumptionInfo *)findResumptionInfoByResumptionID:(NSData *)resumptionID; +- (void)storeResumptionInfo:(MTRCASESessionResumptionInfo *)resumptionInfo; +- (void)clearAllResumptionInfo; + +/** + * Storage of the last NOC we used on this device. This is local-only storage, + * because it's used to invalidate (or not) the local-only session resumption + * storage. + */ +- (CHIP_ERROR)storeLastLocallyUsedNOC:(MTRCertificateTLVBytes)noc; +- (MTRCertificateTLVBytes _Nullable)fetchLastLocallyUsedNOC; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm new file mode 100644 index 00000000000000..4732009ade3513 --- /dev/null +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerDataStore.mm @@ -0,0 +1,359 @@ +/** + * 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" +#import "MTRLogging_Internal.h" + +#include +#include +#include + +// 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(unsignedValue)) { + return false; + } + + auto tag = static_cast(unsignedValue); + if (!chip::IsValidCASEAuthTag(tag)) { + return false; + } + + return true; +} + +@implementation MTRDeviceControllerDataStore { + id _storageDelegate; + dispatch_queue_t _storageDelegateQueue; + MTRDeviceController * _controller; + // Array of nodes with resumption info, oldest-stored first. + NSMutableArray * _nodesWithResumptionInfo; +} + +- (nullable instancetype)initWithController:(MTRDeviceController *)controller + storageDelegate:(id)storageDelegate + storageDelegateQueue:(dispatch_queue_t)storageDelegateQueue +{ + if (!(self = [super init])) { + return nil; + } + + _controller = controller; + _storageDelegate = storageDelegate; + _storageDelegateQueue = storageDelegateQueue; + + __block id resumptionNodeList; + dispatch_sync(_storageDelegateQueue, ^{ + 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 = [NSMutableArray arrayWithCapacity:[resumptionNodeList count]]; + [_nodesWithResumptionInfo addObjectsFromArray:resumptionNodeList]; + } else { + _nodesWithResumptionInfo = [[NSMutableArray alloc] init]; + } + return self; +} + +- (nullable MTRCASESessionResumptionInfo *)findResumptionInfoByNodeID:(NSNumber *)nodeID +{ + return [self _findResumptionInfoWithKey:ResumptionByNodeIDKey(nodeID)]; +} + +- (nullable MTRCASESessionResumptionInfo *)findResumptionInfoByResumptionID:(NSData *)resumptionID +{ + return [self _findResumptionInfoWithKey:ResumptionByResumptionIDKey(resumptionID)]; +} + +- (void)storeResumptionInfo:(MTRCASESessionResumptionInfo *)resumptionInfo +{ + 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:[NSArray arrayWithArray:_nodesWithResumptionInfo] + forKey:sResumptionNodeListKey + securityLevel:MTRStorageSecurityLevelSecure + sharingType:MTRStorageSharingTypeNotShared]; + }); +} + +- (void)clearAllResumptionInfo +{ + // 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 +{ + __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 +{ + __block id data; + dispatch_sync(_storageDelegateQueue, ^{ + 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 +{ + // key could be nil if [NSString stringWithFormat] returns nil for some reason. + if (key == nil) { + return nil; + } + + __block id resumptionInfo; + dispatch_sync(_storageDelegateQueue, ^{ + resumptionInfo = [_storageDelegate controller:_controller + valueForKey:key + securityLevel:MTRStorageSecurityLevelSecure + sharingType:MTRStorageSharingTypeNotShared]; + }); + + if (resumptionInfo == nil) { + return nil; + } + + if (![resumptionInfo isKindOfClass:[MTRCASESessionResumptionInfo class]]) { + return nil; + } + + return resumptionInfo; +} + +@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 * MTRDeviceControllerStorageClasses() +{ + static NSSet * const sStorageClasses = [NSSet setWithArray:@[ + [NSNumber class], + [NSData class], + [NSArray class], + [MTRCASESessionResumptionInfo class], + ]]; + return sStorageClasses; +} diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.h b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.h index 0423aeae585c70..6471db8b005ccb 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.h +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.h @@ -22,6 +22,7 @@ #import #import +#import NS_ASSUME_NONNULL_BEGIN @@ -37,9 +38,11 @@ NS_ASSUME_NONNULL_BEGIN API_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)) @interface MTRDeviceControllerFactoryParams : NSObject /* - * Storage delegate must be provided for correct functioning of Matter - * controllers. It is used to store persistent information for the fabrics the - * controllers ends up interacting with. + * Storage used to store persistent information for the fabrics the + * controllers ends up interacting with. This is only used if "initWithStorage" + * is used to initialize the MTRDeviceControllerFactoryParams. If "init" is + * used, this property will contain a dummy storage that will not be used for + * anything. */ @property (nonatomic, strong, readonly) id storage; @@ -83,8 +86,19 @@ API_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)) */ @property (nonatomic, assign) BOOL shouldStartServer; -- (instancetype)init NS_UNAVAILABLE; +/* + * Initialize the device controller factory with storage. In this mode, the + * storage will be used to store various information needed by the Matter + * framework. + */ - (instancetype)initWithStorage:(id)storage; + +/* + * Initialize the device controller factory without storage. In this mode, + * device controllers will need to have per-controller storage provided to allow + * storing controller-specific information. + */ +- (instancetype)init MTR_NEWLY_AVAILABLE; @end API_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)) @@ -145,6 +159,9 @@ API_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)) * * The fabric is identified by the root public key and fabric id in * the startupParams. + * + * This method can only be used if the factory was initialized with storage. + * When using per-controller storage, use createController. */ - (MTRDeviceController * _Nullable)createControllerOnExistingFabric:(MTRDeviceControllerStartupParams *)startupParams error:(NSError * __autoreleasing *)error; @@ -156,10 +173,25 @@ API_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)) * * The fabric is identified by the root public key and fabric id in * the startupParams. + * + * This method can only be used if the factory was initialized with storage. + * When using per-controller storage, use createController. */ - (MTRDeviceController * _Nullable)createControllerOnNewFabric:(MTRDeviceControllerStartupParams *)startupParams error:(NSError * __autoreleasing *)error; +/** + * Create an MTRDeviceController. Returns nil on failure. + * + * This method will fail if there is already a controller running for the given + * node identity. + * + * This method will fail if the controller factory was not initialized in + * storage-per-controller mode. + */ +- (MTRDeviceController * _Nullable)createController:(MTRDeviceControllerStartupParameters *)startupParameters + error:(NSError * __autoreleasing *)error MTR_NEWLY_AVAILABLE; + @end MTR_DEPRECATED( diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm index 64e4af1ba29fc9..95282450952ef2 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm @@ -20,6 +20,7 @@ #import "MTRAttestationTrustStoreBridge.h" #import "MTRCertificates.h" #import "MTRControllerAccessControl.h" +#import "MTRDemuxingStorage.h" #import "MTRDeviceController.h" #import "MTRDeviceControllerStartupParams.h" #import "MTRDeviceControllerStartupParams_Internal.h" @@ -32,6 +33,7 @@ #import "MTROperationalBrowser.h" #import "MTRP256KeypairBridge.h" #import "MTRPersistentStorageDelegateBridge.h" +#import "MTRSessionResumptionStorageBridge.h" #import "NSDataSpanConversion.h" #import @@ -55,6 +57,7 @@ using namespace chip::Controller; static NSString * const kErrorPersistentStorageInit = @"Init failure while creating a persistent storage delegate"; +static NSString * const kErrorSessionResumptionStorageInit = @"Init failure while creating a session resumption storage delegate"; static NSString * const kErrorAttestationTrustStoreInit = @"Init failure while creating the attestation trust store"; static NSString * const kErrorDACVerifierInit = @"Init failure while creating the device attestation verifier"; static NSString * const kErrorGroupProviderInit = @"Init failure while initializing group data provider"; @@ -90,21 +93,36 @@ @interface MTRDeviceControllerFactory () @property () chip::Credentials::DeviceAttestationVerifier * deviceAttestationVerifier; @property (readonly) BOOL advertiseOperational; @property (nonatomic, readonly) Credentials::IgnoreCertificateValidityPeriodPolicy * certificateValidityPolicy; -// Lock used to serialize access to the "controllers" array, since it needs to -// be touched from both whatever queue is starting controllers and from the -// Matter queue. The way this lock is used assumes that: +@property (readonly) MTRSessionResumptionStorageBridge * sessionResumptionStorage; +// Lock used to serialize access to the "controllers" array and the +// "_controllerBeingStarted" and "_controllerBeingShutDown" ivars, since those +// need to be touched from both whatever queue is starting controllers and from +// the Matter queue. The way this lock is used assumes that: +// +// 1) The only mutating accesses to the controllers array and the ivars happen +// when the current queue is not the Matter queue or in a block that was +// sync-dispatched to the Matter queue. This is a good assumption, because +// the implementations of the functions that mutate these do sync dispatch to +// the Matter queue, which would deadlock if they were called when that queue +// was the current queue. // -// 1) The only mutating accesses to the controllers array happen when the -// current queue is not the Matter queue. This is a good assumption, because -// the implementation of the functions that mutate the array do sync dispatch -// to the Matter queue, which would deadlock if they were called when that -// queue was the current queue. // 2) It's our API consumer's responsibility to serialize access to us from // outside. // -// This means that we only take the lock around mutations of the array and -// accesses to the array that are from code running on the Matter queue. - +// These assumptions mean that if we are in a block that was sync-dispatched to +// the Matter queue, that block cannot race with either the Matter queue nor the +// non-Matter queue. Similarly, if we are in a situation where the Matter queue +// has been shut down, any accesses to the variables cannot race anything else. +// +// This means that: +// +// A. In a sync-dispatched block, or if the Matter queue has been shut down, we +// do not need to lock and can do read or write access. +// B. Apart from item A, mutations of the array and ivars must happen outside the +// Matter queue and must lock. +// C. Apart from item A, accesses on the Matter queue must be reads only and +// must lock. +// D. Locking around reads not from the Matter queue is OK but not required. @property (nonatomic, readonly) os_unfair_lock controllersLock; - (BOOL)findMatchingFabric:(FabricTable &)fabricTable @@ -114,7 +132,34 @@ - (BOOL)findMatchingFabric:(FabricTable &)fabricTable - (MTRDeviceController * _Nullable)maybeInitializeOTAProvider:(MTRDeviceController * _Nonnull)controller; @end -@implementation MTRDeviceControllerFactory +@interface MTRDeviceControllerFactoryParams () + +// Flag to keep track of whether our .storage is real consumer-provided storage +// or just the fake thing we made up. +@property (nonatomic, assign) BOOL hasStorage; + +@end + +@implementation MTRDeviceControllerFactory { + // _usingPerControllerStorage is only written once, during controller + // factory start. After that it is only read, and can be read from + // arbitrary threads. + BOOL _usingPerControllerStorage; + + // See documentation for controllersLock above for the rules for accessing + // _controllerBeingStarted. + MTRDeviceController * _controllerBeingStarted; + + // See documentation for controllersLock above for the rules for access + // _controllerBeingShutDown. + MTRDeviceController * _controllerBeingShutDown; + + // Next available fabric index. Only valid when _controllerBeingStarted is + // non-nil, and then it corresponds to the controller being started. This + // is only accessed on the Matter queue or after the Matter queue has shut + // down. + FabricIndex _nextAvailableFabricIndex; +} + (void)initialize { @@ -273,12 +318,23 @@ - (void)cleanupStartupObjects _opCertStore = nullptr; } + if (_sessionResumptionStorage) { + delete _sessionResumptionStorage; + _sessionResumptionStorage = nullptr; + } + if (_persistentStorageDelegate) { delete _persistentStorageDelegate; _persistentStorageDelegate = nullptr; } } +- (CHIP_ERROR)_initFabricTable:(FabricTable &)fabricTable +{ + return fabricTable.Init( + { .storage = _persistentStorageDelegate, .operationalKeystore = _keystore, .opCertStore = _opCertStore }); +} + - (nullable NSArray *)knownFabrics { [self _assertCurrentQueueIsNotMatterQueue]; @@ -291,9 +347,7 @@ - (void)cleanupStartupObjects __block BOOL listFilled = NO; auto fillListBlock = ^{ FabricTable fabricTable; - CHIP_ERROR err = fabricTable.Init({ .storage = self->_persistentStorageDelegate, - .operationalKeystore = self->_keystore, - .opCertStore = self->_opCertStore }); + CHIP_ERROR err = [self _initFabricTable:fabricTable]; if (err != CHIP_NO_ERROR) { MTR_LOG_ERROR("Can't initialize fabric table when getting known fabrics: %s", err.AsString()); return; @@ -348,7 +402,22 @@ - (BOOL)startControllerFactory:(MTRDeviceControllerFactoryParams *)startupParams [MTRControllerAccessControl init]; - _persistentStorageDelegate = new MTRPersistentStorageDelegateBridge(startupParams.storage); + if (startupParams.hasStorage) { + _persistentStorageDelegate = new (std::nothrow) MTRPersistentStorageDelegateBridge(startupParams.storage); + _sessionResumptionStorage = nullptr; + _usingPerControllerStorage = NO; + } else { + _persistentStorageDelegate = new (std::nothrow) MTRDemuxingStorage(self); + _sessionResumptionStorage = new (std::nothrow) MTRSessionResumptionStorageBridge(self); + _usingPerControllerStorage = YES; + + if (_sessionResumptionStorage == nil) { + MTR_LOG_ERROR("Error: %@", kErrorSessionResumptionStorageInit); + errorCode = CHIP_ERROR_NO_MEMORY; + return; + } + } + if (_persistentStorageDelegate == nil) { MTR_LOG_ERROR("Error: %@", kErrorPersistentStorageInit); errorCode = CHIP_ERROR_NO_MEMORY; @@ -486,6 +555,7 @@ - (BOOL)startControllerFactory:(MTRDeviceControllerFactoryParams *)startupParams params.operationalKeystore = _keystore; params.opCertStore = _opCertStore; params.certificateValidityPolicy = _certificateValidityPolicy; + params.sessionResumptionStorage = _sessionResumptionStorage; errorCode = _controllerFactory->Init(params); if (errorCode != CHIP_NO_ERROR) { MTR_LOG_ERROR("Error: %@", kErrorControllerFactoryInit); @@ -559,7 +629,7 @@ - (void)stopControllerFactory * return nil if pre-startup fabric table checks fail, and set fabricError to * the right error value in that situation. */ -- (MTRDeviceController * _Nullable)_startDeviceController:(MTRDeviceControllerStartupParams *)startupParams +- (MTRDeviceController * _Nullable)_startDeviceController:(id)startupParams fabricChecker:(MTRDeviceControllerStartupParamsInternal * (^)(FabricTable * fabricTable, MTRDeviceController * controller, CHIP_ERROR & fabricError))fabricChecker @@ -572,9 +642,45 @@ - (MTRDeviceController * _Nullable)_startDeviceController:(MTRDeviceControllerSt return nil; } + id storageDelegate; + dispatch_queue_t storageDelegateQueue; + NSUUID * uniqueIdentifier; + if ([startupParams isKindOfClass:[MTRDeviceControllerStartupParameters class]]) { + MTRDeviceControllerStartupParameters * params = startupParams; + storageDelegate = params.storageDelegate; + storageDelegateQueue = params.storageDelegateQueue; + uniqueIdentifier = params.uniqueIdentifier; + } else if ([startupParams isKindOfClass:[MTRDeviceControllerStartupParams class]]) { + MTRDeviceControllerStartupParams * params = startupParams; + storageDelegate = nil; + storageDelegateQueue = nil; + uniqueIdentifier = params.uniqueIdentifier; + } else { + MTR_LOG_ERROR("Unknown kind of startup params: %@", startupParams); + return nil; + } + + if (_usingPerControllerStorage && storageDelegate == nil) { + MTR_LOG_ERROR("Must have a controller storage delegate when we do not have storage for the controller factory"); + if (error != nil) { + *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT]; + } + return nil; + } + + if (!_usingPerControllerStorage && storageDelegate != nil) { + MTR_LOG_ERROR("Must not have a controller storage delegate when we have storage for the controller factory"); + if (error != nil) { + *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT]; + } + return nil; + } + // Create the controller, so we start the event loop, since we plan to do // our fabric table operations there. - auto * controller = [self createController]; + auto * controller = [self _createController:storageDelegate + storageDelegateQueue:storageDelegateQueue + uniqueIdentifier:uniqueIdentifier]; if (controller == nil) { if (error != nil) { *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_NO_MEMORY]; @@ -591,7 +697,39 @@ - (MTRDeviceController * _Nullable)_startDeviceController:(MTRDeviceControllerSt FabricTable * fabricTable = &fabricTableInstance; dispatch_sync(_chipWorkQueue, ^{ + fabricError = [self _initFabricTable:*fabricTable]; + if (fabricError != CHIP_NO_ERROR) { + MTR_LOG_ERROR("Can't initialize fabric table: %s", fabricError.AsString()); + return; + } + params = fabricChecker(fabricTable, controller, fabricError); + + if (params == nil) { + return; + } + + // Check that we are not trying to start a controller with a uniqueIdentifier that + // matches a running controller. + auto * controllersCopy = [self getRunningControllers]; + for (MTRDeviceController * existing in controllersCopy) { + if (existing != controller && [existing.uniqueIdentifier compare:params.uniqueIdentifier] == NSOrderedSame) { + MTR_LOG_ERROR("Already have running controller with uniqueIdentifier %@", existing.uniqueIdentifier); + fabricError = CHIP_ERROR_INVALID_ARGUMENT; + params = nil; + return; + } + } + + // Save off the next available fabric index, in case we are starting a + // controller with a new fabric index. This just needs to happen before + // we set _controllerBeingStarted below. + fabricError = fabricTable->PeekFabricIndexForNextAddition(self->_nextAvailableFabricIndex); + if (fabricError != CHIP_NO_ERROR) { + MTR_LOG_ERROR("Out of space in the fabric table"); + params = nil; + return; + } }); if (params == nil) { @@ -602,7 +740,16 @@ - (MTRDeviceController * _Nullable)_startDeviceController:(MTRDeviceControllerSt return nil; } + os_unfair_lock_lock(&_controllersLock); + _controllerBeingStarted = controller; + os_unfair_lock_unlock(&_controllersLock); + BOOL ok = [controller startup:params]; + + os_unfair_lock_lock(&_controllersLock); + _controllerBeingStarted = nil; + os_unfair_lock_unlock(&_controllersLock); + if (ok == NO) { // TODO: get error from controller's startup. if (error != nil) { @@ -625,6 +772,16 @@ - (MTRDeviceController * _Nullable)createControllerOnExistingFabric:(MTRDeviceCo { [self _assertCurrentQueueIsNotMatterQueue]; + if (_usingPerControllerStorage) { + // We can never have an "existing fabric" for a new controller to be + // created on, in the sense of createControllerOnExistingFabric. + MTR_LOG_ERROR("Can't createControllerOnExistingFabric when using per-controller data store"); + if (error != nil) { + *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INCORRECT_STATE]; + } + return nil; + } + return [self _startDeviceController:startupParams fabricChecker:^MTRDeviceControllerStartupParamsInternal *( FabricTable * fabricTable, MTRDeviceController * controller, CHIP_ERROR & fabricError) { @@ -728,11 +885,36 @@ - (MTRDeviceController * _Nullable)createControllerOnNewFabric:(MTRDeviceControl error:error]; } -- (MTRDeviceController * _Nullable)createController +- (MTRDeviceController * _Nullable)createController:(MTRDeviceControllerStartupParameters *)startupParameters + error:(NSError * __autoreleasing *)error { [self _assertCurrentQueueIsNotMatterQueue]; - MTRDeviceController * controller = [[MTRDeviceController alloc] initWithFactory:self queue:_chipWorkQueue]; + return [self _startDeviceController:startupParameters + fabricChecker:^MTRDeviceControllerStartupParamsInternal *( + FabricTable * fabricTable, MTRDeviceController * controller, CHIP_ERROR & fabricError) { + return + [[MTRDeviceControllerStartupParamsInternal alloc] initForNewController:controller + fabricTable:fabricTable + keystore:self->_keystore + advertiseOperational:self.advertiseOperational + params:startupParameters + error:fabricError]; + } + error:error]; +} + +- (MTRDeviceController * _Nullable)_createController:(id _Nullable)storageDelegate + storageDelegateQueue:(dispatch_queue_t _Nullable)storageDelegateQueue + uniqueIdentifier:(NSUUID *)uniqueIdentifier +{ + [self _assertCurrentQueueIsNotMatterQueue]; + + MTRDeviceController * controller = [[MTRDeviceController alloc] initWithFactory:self + queue:_chipWorkQueue + storageDelegate:storageDelegate + storageDelegateQueue:storageDelegateQueue + uniqueIdentifier:uniqueIdentifier]; if (controller == nil) { MTR_LOG_ERROR("Failed to init controller"); return nil; @@ -761,7 +943,7 @@ - (MTRDeviceController * _Nullable)createController // Returns NO on failure, YES on success. If YES is returned, the // outparam will be written to, but possibly with a null value. // -// fabricTable should be an un-initialized fabric table. It needs to +// fabricTable should be an initialized fabric table. It needs to // outlive the consumer's use of the FabricInfo we return, which is // why it's provided by the caller. - (BOOL)findMatchingFabric:(FabricTable &)fabricTable @@ -770,16 +952,9 @@ - (BOOL)findMatchingFabric:(FabricTable &)fabricTable { assertChipStackLockedByCurrentThread(); - CHIP_ERROR err = fabricTable.Init( - { .storage = _persistentStorageDelegate, .operationalKeystore = _keystore, .opCertStore = _opCertStore }); - if (err != CHIP_NO_ERROR) { - MTR_LOG_ERROR("Can't initialize fabric table: %s", ErrorStr(err)); - return NO; - } - Crypto::P256PublicKey pubKey; if (params.rootCertificate != nil) { - err = ExtractPubkeyFromX509Cert(AsByteSpan(params.rootCertificate), pubKey); + CHIP_ERROR err = ExtractPubkeyFromX509Cert(AsByteSpan(params.rootCertificate), pubKey); if (err != CHIP_NO_ERROR) { MTR_LOG_ERROR("Can't extract public key from root certificate: %s", ErrorStr(err)); return NO; @@ -787,7 +962,7 @@ - (BOOL)findMatchingFabric:(FabricTable &)fabricTable } else { // No root certificate means the nocSigner is using the root keys, because // consumers must provide a root certificate whenever an ICA is used. - err = MTRP256KeypairBridge::MatterPubKeyFromSecKeyRef(params.nocSigner.publicKey, &pubKey); + CHIP_ERROR err = MTRP256KeypairBridge::MatterPubKeyFromSecKeyRef(params.nocSigner.publicKey, &pubKey); if (err != CHIP_NO_ERROR) { MTR_LOG_ERROR("Can't extract public key from MTRKeypair: %s", ErrorStr(err)); return NO; @@ -850,9 +1025,33 @@ - (void)controllerShuttingDown:(MTRDeviceController *)controller } os_unfair_lock_lock(&_controllersLock); + // Make sure to set _controllerBeingShutDown and do the remove in the same + // locked section, so there is never a time when the controller is gone from + // both places as viewed from the Matter thread, as long as it's locking + // around its reads. + _controllerBeingShutDown = controller; [_controllers removeObject:controller]; os_unfair_lock_unlock(&_controllersLock); + // Snapshot the controller's fabric index, if any, before it clears it + // out in shutDownCppController. + __block FabricIndex controllerFabricIndex = controller.fabricIndex; + + // This block runs either during sync dispatch to the Matter queue or after + // Matter queue shutdown, so it can touch any of our members without + // worrying about locking, since nothing else will race it. + auto sharedCleanupBlock = ^{ + assertChipStackLockedByCurrentThread(); + + [controller shutDownCppController]; + + self->_controllerBeingShutDown = nil; + if (self->_controllerBeingStarted == controller) { + controllerFabricIndex = self->_nextAvailableFabricIndex; + self->_controllerBeingStarted = nil; + } + }; + if ([_controllers count] == 0) { dispatch_sync(_chipWorkQueue, ^{ delete self->_operationalBrowser; @@ -867,7 +1066,23 @@ - (void)controllerShuttingDown:(MTRDeviceController *)controller _otaProviderDelegateBridge->Shutdown(); } - [controller shutDownCppController]; + sharedCleanupBlock(); + + // Now that our per-controller storage for the controller being shut + // down is guaranteed to be disconnected, go ahead and clean up the + // fabric table entry for the controller if we're in per-controller + // storage mode. + if (self->_usingPerControllerStorage) { + // We have to use a new fabric table to do this cleanup, because + // our system state is gone now. + FabricTable fabricTable; + CHIP_ERROR err = [self _initFabricTable:fabricTable]; + if (err != CHIP_NO_ERROR) { + MTR_LOG_ERROR("Failed to clean up fabric entries. Expect things to act oddly: %" CHIP_ERROR_FORMAT, err.Format()); + } else { + fabricTable.Delete(controllerFabricIndex); + } + } } else { // Do the controller shutdown on the Matter work queue. dispatch_sync(_chipWorkQueue, ^{ @@ -875,7 +1090,19 @@ - (void)controllerShuttingDown:(MTRDeviceController *)controller _otaProviderDelegateBridge->ControllerShuttingDown(controller); } - [controller shutDownCppController]; + sharedCleanupBlock(); + + // Now that our per-controller storage for the controller being shut + // down is guaranteed to be disconnected, go ahead and clean up the + // fabric table entry for the controller if we're in per-controller + // storage mode. + if (self->_usingPerControllerStorage) { + // Make sure to delete controllerFabricIndex from the system state's + // fabric table. We know there's a system state here, because we + // still have a running controller. + auto * systemState = _controllerFactory->GetSystemState(); + systemState->Fabrics()->Delete(controllerFabricIndex); + } }); } @@ -890,21 +1117,42 @@ - (void)controllerShuttingDown:(MTRDeviceController *)controller return controllersCopy; } -- (nullable MTRDeviceController *)runningControllerForFabricIndex:(chip::FabricIndex)fabricIndex +- (nullable MTRDeviceController *)runningControllerForFabricIndex:(FabricIndex)fabricIndex + includeControllerStartingUp:(BOOL)includeControllerStartingUp + includeControllerShuttingDown:(BOOL)includeControllerShuttingDown { assertChipStackLockedByCurrentThread(); auto * controllersCopy = [self getRunningControllers]; + os_unfair_lock_lock(&_controllersLock); + MTRDeviceController * controllerBeingStarted = _controllerBeingStarted; + MTRDeviceController * controllerBeingShutDown = _controllerBeingShutDown; + os_unfair_lock_unlock(&_controllersLock); + for (MTRDeviceController * existing in controllersCopy) { - if ([existing fabricIndex] == fabricIndex) { + if (existing.fabricIndex == fabricIndex) { return existing; } } + if (includeControllerStartingUp == YES && controllerBeingStarted != nil && fabricIndex == _nextAvailableFabricIndex) { + return controllerBeingStarted; + } + + if (includeControllerShuttingDown == YES && controllerBeingShutDown != nil + && controllerBeingShutDown.fabricIndex == fabricIndex) { + return controllerBeingShutDown; + } + return nil; } +- (nullable MTRDeviceController *)runningControllerForFabricIndex:(chip::FabricIndex)fabricIndex +{ + return [self runningControllerForFabricIndex:fabricIndex includeControllerStartingUp:YES includeControllerShuttingDown:YES]; +} + - (void)operationalInstanceAdded:(chip::PeerId &)operationalID { assertChipStackLockedByCurrentThread(); @@ -936,6 +1184,25 @@ - (PersistentStorageDelegate *)storageDelegate @end +MTR_HIDDEN +@interface MTRDummyStorage : NSObject +@end + +@implementation MTRDummyStorage +- (nullable NSData *)storageDataForKey:(NSString *)key +{ + return nil; +} +- (BOOL)setStorageData:(NSData *)value forKey:(NSString *)key +{ + return NO; +} +- (BOOL)removeStorageDataForKey:(NSString *)key +{ + return NO; +} +@end + @implementation MTRDeviceControllerFactoryParams - (instancetype)initWithStorage:(id)storage @@ -945,6 +1212,27 @@ - (instancetype)initWithStorage:(id)storage } _storage = storage; + _hasStorage = YES; + _otaProviderDelegate = nil; + _productAttestationAuthorityCertificates = nil; + _certificationDeclarationCertificates = nil; + _port = nil; + _shouldStartServer = NO; + + return self; +} + +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + + // We promise to have a non-null storage for purposes of our attribute, but + // now we're allowing initialization without storage. Make up a dummy + // storage just so we don't have nil there. + _storage = [[MTRDummyStorage alloc] init]; + _hasStorage = NO; _otaProviderDelegate = nil; _productAttestationAuthorityCertificates = nil; _certificationDeclarationCertificates = nil; diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory_Internal.h b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory_Internal.h index dc0b968b427782..4ffcb3af545506 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory_Internal.h +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory_Internal.h @@ -53,6 +53,15 @@ NS_ASSUME_NONNULL_BEGIN */ - (nullable MTRDeviceController *)runningControllerForFabricIndex:(chip::FabricIndex)fabricIndex; +/** + * Find a running controller, if any, for the given fabric index. Allows + * controlling whether to include a controller that is in the middle of startup + * or shutdown. + */ +- (nullable MTRDeviceController *)runningControllerForFabricIndex:(chip::FabricIndex)fabricIndex + includeControllerStartingUp:(BOOL)includeControllerStartingUp + includeControllerShuttingDown:(BOOL)includeControllerShuttingDown; + /** * Notify the controller factory that a new operational instance with the given * compressed fabric id and node id has been observed. diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParameters.h b/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParameters.h new file mode 100644 index 00000000000000..7087bd87fbaf8e --- /dev/null +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParameters.h @@ -0,0 +1,89 @@ +/** + * 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. + */ + +#import + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +MTR_NEWLY_AVAILABLE +@interface MTRDeviceControllerStartupParameters : NSObject + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +/** + * Set an MTROperationalCertificateIssuer to call (on the provided queue) when + * operational certificates need to be provided during commissioning. + */ +- (void)setOperationalCertificateIssuer:(id)operationalCertificateIssuer + queue:(dispatch_queue_t)queue; + +@end + +MTR_NEWLY_AVAILABLE +@interface MTRDeviceControllerExternalCertificateStartupParameters : MTRDeviceControllerStartupParameters + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +/** + * Prepare to initialize a controller that is not able to sign operational + * certificates itself, and therefore needs to be provided with a complete + * operational certificate chain. + * + * A controller created from MTRDeviceControllerStartupParams initialized with + * this method will not be able to commission devices unless + * operationalCertificateIssuer and operationalCertificateIssuerQueue are set. + * + * The fabric id and node id to use for the controller will be derived from the provided + * operationalCertificate. + * + * @param storageDelegate The storage to use for the controller. This will be + * called into on storageDelegateQueue. + * + * @param storageDelegateQueue The queue for calls into storageDelegate. See + * MTRDeviceControllerStorageDelegate documentation + * for the rules about what work is allowed to be + * done on this queue. + * + * @param uniqueIdentifier The unique id to assign to the controller. + * + * @param vendorID The vendor ID (allocated by the Connectivity Standards Alliance) for + * this controller. Must not be the "standard" vendor id (0). + * + * @param ipk The Identity Protection Key. Must be 16 bytes in length. + * + * @param intermediateCertificate Must be nil if operationalCertificate is + * directly signed by rootCertificate. Otherwise + * must be the certificate that signed + * operationalCertificate. + */ +- (instancetype)initWithStorageDelegate:(id)storageDelegate + storageDelegateQueue:(dispatch_queue_t)storageDelegateQueue + uniqueIdentifier:(NSUUID *)uniqueIdentifier + ipk:(NSData *)ipk + vendorID:(NSNumber *)vendorID + operationalKeypair:(id)operationalKeypair + operationalCertificate:(MTRCertificateDERBytes)operationalCertificate + intermediateCertificate:(MTRCertificateDERBytes _Nullable)intermediateCertificate + rootCertificate:(MTRCertificateDERBytes)rootCertificate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams.mm b/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams.mm index ca92c63bf2171b..9fb71429a07720 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams.mm @@ -18,10 +18,13 @@ #import "MTRCertificates.h" #import "MTRConversion.h" #import "MTRDeviceControllerStartupParams_Internal.h" +#import "MTRDeviceController_Internal.h" #import "MTRLogging_Internal.h" #import "MTRP256KeypairBridge.h" #import "NSDataSpanConversion.h" +#import + #include #include #include @@ -29,6 +32,41 @@ using namespace chip; +static CHIP_ERROR ExtractNodeIDFabricIDFromNOC( + MTRCertificateDERBytes noc, NSNumber * __autoreleasing * nodeID, NSNumber * __autoreleasing * fabricID) +{ + // ExtractNodeIdFabricIdFromOpCert needs a TLV-encoded opcert, not a DER-encoded one. + auto * tlvNOC = [MTRCertificates convertX509Certificate:noc]; + if (tlvNOC == nil) { + return CHIP_ERROR_INVALID_ARGUMENT; + } + + ByteSpan nocSpan = AsByteSpan(tlvNOC); + + FabricId certFabricID = kUndefinedFabricId; + NodeId certNodeID = kUndefinedNodeId; + CHIP_ERROR err = Credentials::ExtractNodeIdFabricIdFromOpCert(nocSpan, &certNodeID, &certFabricID); + if (err != CHIP_NO_ERROR) { + MTR_LOG_ERROR("Unable to extract node ID and fabric ID from operational certificate: %s", err.AsString()); + return err; + } + *nodeID = @(certNodeID); + *fabricID = @(certFabricID); + return CHIP_NO_ERROR; +} + +static CHIP_ERROR ExtractFabricIDFromNOC(MTRCertificateDERBytes noc, NSNumber * __autoreleasing * fabricID) +{ + NSNumber * ignored; + return ExtractNodeIDFabricIDFromNOC(noc, &ignored, fabricID); +} + +static CHIP_ERROR ExtractNodeIDFromNOC(MTRCertificateDERBytes noc, NSNumber * __autoreleasing * nodeID) +{ + NSNumber * ignored; + return ExtractNodeIDFabricIDFromNOC(noc, nodeID, &ignored); +} + @implementation MTRDeviceControllerStartupParams - (instancetype)initWithIPK:(NSData *)ipk fabricID:(NSNumber *)fabricID nocSigner:(id)nocSigner @@ -45,6 +83,7 @@ - (instancetype)initWithIPK:(NSData *)ipk fabricID:(NSNumber *)fabricID nocSigne _nocSigner = nocSigner; _fabricID = [fabricID copy]; _ipk = [ipk copy]; + _uniqueIdentifier = [NSUUID UUID]; return self; } @@ -59,24 +98,13 @@ - (instancetype)initWithIPK:(NSData *)ipk return nil; } - { // Scope for temporaries - // ExtractNodeIdFabricIdFromOpCert needs a TLV-encoded opcert, not a DER-encoded one. - uint8_t tlvOpCertBuf[Credentials::kMaxCHIPCertLength]; - MutableByteSpan tlvOpCert(tlvOpCertBuf); - CHIP_ERROR err = Credentials::ConvertX509CertToChipCert(AsByteSpan(operationalCertificate), tlvOpCert); + { // Scope for temporary + NSNumber * fabricID; + CHIP_ERROR err = ExtractFabricIDFromNOC(operationalCertificate, &fabricID); if (err != CHIP_NO_ERROR) { - MTR_LOG_ERROR("Unable to convert operational certificate to TLV: %s", ErrorStr(err)); return nil; } - - FabricId fabricId = kUndefinedFabricId; - NodeId unused = kUndefinedNodeId; - err = Credentials::ExtractNodeIdFabricIdFromOpCert(tlvOpCert, &unused, &fabricId); - if (err != CHIP_NO_ERROR) { - MTR_LOG_ERROR("Unable to extract fabric id from operational certificate: %s", ErrorStr(err)); - return nil; - } - _fabricID = @(fabricId); + _fabricID = fabricID; } _operationalKeypair = operationalKeypair; @@ -84,6 +112,7 @@ - (instancetype)initWithIPK:(NSData *)ipk _intermediateCertificate = [intermediateCertificate copy]; _rootCertificate = [rootCertificate copy]; _ipk = [ipk copy]; + _uniqueIdentifier = [NSUUID UUID]; return self; } @@ -106,6 +135,50 @@ - (instancetype)initWithParams:(MTRDeviceControllerStartupParams *)params _operationalKeypair = params.operationalKeypair; _operationalCertificateIssuer = params.operationalCertificateIssuer; _operationalCertificateIssuerQueue = params.operationalCertificateIssuerQueue; + _uniqueIdentifier = params.uniqueIdentifier; + + return self; +} + +- (instancetype)initWithParameters:(MTRDeviceControllerStartupParameters *)params error:(CHIP_ERROR &)error +{ + if (!(self = [super init])) { + error = CHIP_ERROR_INCORRECT_STATE; + return nil; + } + + if (![params isKindOfClass:[MTRDeviceControllerExternalCertificateStartupParameters class]]) { + MTR_LOG_ERROR("Unexpected subclass of MTRDeviceControllerStartupParameters"); + error = CHIP_ERROR_INVALID_ARGUMENT; + return nil; + } + + _nocSigner = nil; + + NSNumber * fabricID; + error = ExtractFabricIDFromNOC(params.operationalCertificate, &fabricID); + if (error != CHIP_NO_ERROR) { + return nil; + } + _fabricID = fabricID; + + _ipk = params.ipk; + _vendorID = params.vendorID; + // Note: Since we have an operationalCertificate, we do not need a nodeID as + // part of our params; it will not be used. Don't even initialize it, to + // avoid confusion about that. + // + // We don't really use the fabricID for anything either, but we promise to + // have a non-nil one, which is why we set it above. + _nodeID = nil; + _caseAuthenticatedTags = nil; + _rootCertificate = params.rootCertificate; + _intermediateCertificate = params.intermediateCertificate; + _operationalCertificate = params.operationalCertificate; + _operationalKeypair = params.operationalKeypair; + _operationalCertificateIssuer = params.operationalCertificateIssuer; + _operationalCertificateIssuerQueue = params.operationalCertificateIssuerQueue; + _uniqueIdentifier = params.uniqueIdentifier; return self; } @@ -120,7 +193,7 @@ - (instancetype)initWithParams:(MTRDeviceControllerStartupParams *)params MutableByteSpan derCert(buf); CHIP_ERROR err = Credentials::ConvertChipCertToX509Cert(cert, derCert); if (err != CHIP_NO_ERROR) { - MTR_LOG_ERROR("Failed do convert Matter certificate to X.509 DER: %s", ErrorStr(err)); + MTR_LOG_ERROR("Failed to convert Matter certificate to X.509 DER: %s", ErrorStr(err)); return nil; } @@ -174,6 +247,68 @@ - (instancetype)initWithOperationalKeypair:(id)operationalKeypair @end +@implementation MTRDeviceControllerStartupParameters +- (instancetype)initWithStorageDelegate:(id)storageDelegate + storageDelegateQueue:(dispatch_queue_t)storageDelegateQueue + uniqueIdentifier:(NSUUID *)uniqueIdentifier + ipk:(NSData *)ipk + vendorID:(NSNumber *)vendorID + operationalKeypair:(id)operationalKeypair + operationalCertificate:(MTRCertificateDERBytes)operationalCertificate + intermediateCertificate:(MTRCertificateDERBytes _Nullable)intermediateCertificate + rootCertificate:(MTRCertificateDERBytes)rootCertificate +{ + if (!(self = [super init])) { + return nil; + } + + _ipk = ipk; + _vendorID = vendorID; + _rootCertificate = rootCertificate; + _intermediateCertificate = intermediateCertificate; + _operationalCertificate = operationalCertificate; + _operationalKeypair = operationalKeypair; + + _operationalCertificateIssuer = nil; + _operationalCertificateIssuerQueue = nil; + _storageDelegate = storageDelegate; + _storageDelegateQueue = storageDelegateQueue; + _uniqueIdentifier = uniqueIdentifier; + + return self; +} + +- (void)setOperationalCertificateIssuer:(id)operationalCertificateIssuer + queue:(dispatch_queue_t)queue +{ + _operationalCertificateIssuer = operationalCertificateIssuer; + _operationalCertificateIssuerQueue = queue; +} +@end + +@implementation MTRDeviceControllerExternalCertificateStartupParameters +- (instancetype)initWithStorageDelegate:(id)storageDelegate + storageDelegateQueue:(dispatch_queue_t)storageDelegateQueue + uniqueIdentifier:(NSUUID *)uniqueIdentifier + ipk:(NSData *)ipk + vendorID:(NSNumber *)vendorID + operationalKeypair:(id)operationalKeypair + operationalCertificate:(MTRCertificateDERBytes)operationalCertificate + intermediateCertificate:(MTRCertificateDERBytes _Nullable)intermediateCertificate + rootCertificate:(MTRCertificateDERBytes)rootCertificate +{ + return [super initWithStorageDelegate:storageDelegate + storageDelegateQueue:storageDelegateQueue + uniqueIdentifier:uniqueIdentifier + ipk:ipk + vendorID:vendorID + operationalKeypair:operationalKeypair + operationalCertificate:operationalCertificate + intermediateCertificate:intermediateCertificate + rootCertificate:rootCertificate]; +} +@end + @implementation MTRDeviceControllerStartupParamsInternal - (instancetype)initWithParams:(MTRDeviceControllerStartupParams *)params @@ -182,6 +317,9 @@ - (instancetype)initWithParams:(MTRDeviceControllerStartupParams *)params return nil; } + _storageDelegate = nil; + _storageDelegateQueue = nil; + if (self.nocSigner == nil && self.rootCertificate == nil) { MTR_LOG_ERROR("nocSigner and rootCertificate are both nil; no public key available to identify the fabric"); return nil; @@ -249,6 +387,7 @@ - (instancetype)initForNewFabric:(chip::FabricTable *)fabricTable _fabricTable = fabricTable; _keystore = keystore; _advertiseOperational = advertiseOperational; + _allowMultipleControllersPerFabric = NO; return self; } @@ -379,6 +518,89 @@ - (instancetype)initForExistingFabric:(FabricTable *)fabricTable _fabricIndex.Emplace(fabricIndex); _keystore = keystore; _advertiseOperational = advertiseOperational; + _allowMultipleControllersPerFabric = NO; + + return self; +} + +- (instancetype)initForNewController:(MTRDeviceController *)controller + fabricTable:(chip::FabricTable *)fabricTable + keystore:(chip::Crypto::OperationalKeystore *)keystore + advertiseOperational:(BOOL)advertiseOperational + params:(MTRDeviceControllerStartupParameters *)params + error:(CHIP_ERROR &)error +{ + if (!(self = [super initWithParameters:params error:error])) { + return nil; + } + + Crypto::P256PublicKey pubKey; + error = ExtractPubkeyFromX509Cert(AsByteSpan(self.rootCertificate), pubKey); + if (error != CHIP_NO_ERROR) { + MTR_LOG_ERROR("Can't extract public key from root certificate: %s", error.AsString()); + return nil; + } + + NSNumber * nodeID; + error = ExtractNodeIDFromNOC(self.operationalCertificate, &nodeID); + if (error != CHIP_NO_ERROR) { + // Already logged. + return nil; + } + + if (fabricTable->FindIdentity(pubKey, self.fabricID.unsignedLongLongValue, nodeID.unsignedLongLongValue)) { + MTR_LOG_ERROR("Trying to start a controller identity that is already running"); + error = CHIP_ERROR_INVALID_ARGUMENT; + return nil; + } + + auto * oldNOCTLV = [controller.controllerDataStore fetchLastLocallyUsedNOC]; + if (oldNOCTLV != nil) { + ByteSpan oldNOCSpan = AsByteSpan(oldNOCTLV); + + FabricId ignored = kUndefinedFabricId; + NodeId oldNodeID = kUndefinedNodeId; + CHIP_ERROR err = Credentials::ExtractNodeIdFabricIdFromOpCert(oldNOCSpan, &oldNodeID, &ignored); + if (err != CHIP_NO_ERROR) { + MTR_LOG_ERROR("Unable to extract node ID and fabric ID from old operational certificate: %s", err.AsString()); + return nil; + } + + CATValues oldCATs; + err = Credentials::ExtractCATsFromOpCert(oldNOCSpan, oldCATs); + if (err != CHIP_NO_ERROR) { + MTR_LOG_ERROR("Failed to extract CATs from old operational certificate: %s", err.AsString()); + return nil; + } + + auto * tlvNOC = [MTRCertificates convertX509Certificate:self.operationalCertificate]; + if (tlvNOC == nil) { + return nil; + } + + ByteSpan nocSpan = AsByteSpan(tlvNOC); + CATValues newCATs; + err = Credentials::ExtractCATsFromOpCert(nocSpan, newCATs); + if (err != CHIP_NO_ERROR) { + MTR_LOG_ERROR("Failed to extract CATs from new operational certificate: %s", err.AsString()); + return nil; + } + + if (nodeID.unsignedLongLongValue != oldNodeID || oldCATs != newCATs) { + // Our NOC has changed in a way that would affect ACL checks. Clear + // out our session resumption storage, because resuming those CASE + // sessions will end up doing ACL checks against our old NOC. + MTR_LOG_DEFAULT("Node ID or CATs changed. Clearing CASE resumption storage."); + [controller.controllerDataStore clearAllResumptionInfo]; + } + } + + _fabricTable = fabricTable; + _keystore = keystore; + _advertiseOperational = advertiseOperational; + _allowMultipleControllersPerFabric = YES; + _storageDelegate = params.storageDelegate; + _storageDelegateQueue = params.storageDelegateQueue; return self; } diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams_Internal.h b/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams_Internal.h index 24acc7a2dbb3a8..53cec273ebdc52 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams_Internal.h +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams_Internal.h @@ -19,6 +19,8 @@ #import "MTRDeviceControllerStartupParams.h" #import #import +#import +#import #include #include @@ -39,18 +41,52 @@ NS_ASSUME_NONNULL_BEGIN // MTRDeviceControllerStartupParamsInternal. @property (nonatomic, copy, nullable) MTRCertificateDERBytes operationalCertificate; +// uniqueIdentifier, so that we always have one. +@property (nonatomic, strong, readonly) NSUUID * uniqueIdentifier; + // Init method that just copies the values of all our ivars. - (instancetype)initWithParams:(MTRDeviceControllerStartupParams *)params; @end +@interface MTRDeviceControllerStartupParameters () + +- (instancetype)initWithStorageDelegate:(id)storageDelegate + storageDelegateQueue:(dispatch_queue_t)storageDelegateQueue + uniqueIdentifier:(NSUUID *)uniqueIdentifier + ipk:(NSData *)ipk + vendorID:(NSNumber *)vendorID + operationalKeypair:(id)operationalKeypair + operationalCertificate:(MTRCertificateDERBytes)operationalCertificate + intermediateCertificate:(MTRCertificateDERBytes _Nullable)intermediateCertificate + rootCertificate:(MTRCertificateDERBytes)rootCertificate; + +// When we have other subclasses of MTRDeviceControllerStartupParameters, we may +// need to make more things nullable here and/or add more fields. But for now +// we know exactly what information we have. +@property (nonatomic, copy, readonly) NSData * ipk; +@property (nonatomic, copy, readonly) NSNumber * vendorID; +@property (nonatomic, copy, readonly) MTRCertificateDERBytes rootCertificate; +@property (nonatomic, copy, readonly, nullable) MTRCertificateDERBytes intermediateCertificate; +@property (nonatomic, copy, readonly) MTRCertificateDERBytes operationalCertificate; +@property (nonatomic, strong, readonly) id operationalKeypair; + +@property (nonatomic, strong, nullable, readonly) id operationalCertificateIssuer; +@property (nonatomic, strong, nullable, readonly) dispatch_queue_t operationalCertificateIssuerQueue; + +@property (nonatomic, strong, readonly) id storageDelegate; +@property (nonatomic, strong, readonly) dispatch_queue_t storageDelegateQueue; +@property (nonatomic, strong, readonly) NSUUID * uniqueIdentifier; + +@end + MTR_HIDDEN @interface MTRDeviceControllerStartupParamsInternal : MTRDeviceControllerStartupParams // Fabric table we can use to do things like allocate operational keys. @property (nonatomic, assign, readonly) chip::FabricTable * fabricTable; -// Fabric index we're starting on. Only has a value when starting on an -// existing fabric. +// Fabric index we're starting on. Only has a value when starting against an +// existing fabric table entry. @property (nonatomic, assign, readonly) chip::Optional fabricIndex; // Key store we're using with our fabric table, for sanity checks. @@ -58,6 +94,21 @@ MTR_HIDDEN @property (nonatomic, assign, readonly) BOOL advertiseOperational; +@property (nonatomic, assign, readonly) BOOL allowMultipleControllersPerFabric; + +/** + * A storage delegate that can be provided when initializing the startup params. + * This must be provided if and only if the controller factory was initialized + * without storage. + */ +@property (nonatomic, strong, nullable, readonly) id storageDelegate; + +/** + * The queue to use for storageDelegate. This will be nil if and only if + * storageDelegate is nil. + */ +@property (nonatomic, strong, nullable, readonly) dispatch_queue_t storageDelegateQueue; + /** * Helper method that checks that our keypairs match our certificates. * Specifically: @@ -90,7 +141,17 @@ MTR_HIDDEN params:(MTRDeviceControllerStartupParams *)params; /** - * Should use initForExistingFabric or initForNewFabric to initialize + * Initialize for controller bringup with per-controller storage. + */ +- (instancetype)initForNewController:(MTRDeviceController *)controller + fabricTable:(chip::FabricTable *)fabricTable + keystore:(chip::Crypto::OperationalKeystore *)keystore + advertiseOperational:(BOOL)advertiseOperational + params:(MTRDeviceControllerStartupParameters *)params + error:(CHIP_ERROR &)error; + +/** + * Should use initForExistingFabric or initForNewFabric or initForController to initialize * internally. */ - (instancetype)initWithIPK:(NSData *)ipk fabricID:(NSNumber *)fabricID nocSigner:(id)nocSigner NS_UNAVAILABLE; diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerStorageDelegate.h b/src/darwin/Framework/CHIP/MTRDeviceControllerStorageDelegate.h new file mode 100644 index 00000000000000..1fee9c65ae460c --- /dev/null +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerStorageDelegate.h @@ -0,0 +1,110 @@ +/** + * 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. + */ + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, MTRStorageSecurityLevel) { + // Data must be stored in secure (encrypted) storage. + MTRStorageSecurityLevelSecure, + // Data may be stored in the clear. + MTRStorageSecurityLevelNotSecure, +} MTR_NEWLY_AVAILABLE; + +typedef NS_ENUM(NSUInteger, MTRStorageSharingType) { + // Data must not be shared at all (just store locally). + MTRStorageSharingTypeNotShared, + + // Data must be shared, but only between controllers that have the same node + // identity (same fabric, same node ID, same CATs). + MTRStorageSharingTypeSameIdentity, + + // Data must be shared, but only between controllers that have the same + // access to devices (e.g. controllers that all have the same CATs if ACLs + // are being done via CATs). + MTRStorageSharingTypeSameACLs, + + // Data must be shared across all controllers on a given fabric. + MTRStorageSharingTypeSameFabric, +} MTR_NEWLY_AVAILABLE; + +/** + * Protocol for storing and retrieving controller-specific data. + * + * Implementations of this protocol MUST keep two things in mind: + * + * 1) The controller provided to the delegate methods may not be fully + * initialized when the callbacks are called. The only safe thing to do with + * it is to get its controllerID. + * + * 2) The delegate method calls will happen on the queue that was provided along + * with the delegate. All Matter work will be blocked until the method + * completes, and these calls may themselves block other Matter API calls + * from completing. Attempting to call any Matter API on the queue used for + * this delegate, apart from de-serializing and serializing the items being + * stored and calling MTRDeviceControllerStorageClasses(), is likely to lead + * to deadlocks. + */ +MTR_NEWLY_AVAILABLE +@protocol MTRDeviceControllerStorageDelegate +@required +/** + * Return the stored value for the given key, if any, for the provided + * controller. Returns nil if there is no stored value. + * + * securityLevel and dataType will always be the same for any given key value + * and are just present here to help locate the data if storage location is + * separated out by security level and data type. + * + * The set of classes that might be decoded by this function is available by + * calling MTRDeviceControllerStorageClasses(). + */ +- (nullable id)controller:(MTRDeviceController *)controller + valueForKey:(NSString *)key + securityLevel:(MTRStorageSecurityLevel)securityLevel + sharingType:(MTRStorageSharingType)sharingType; + +/** + * Store a value for the given key. Returns whether the store succeeded. + * + * securityLevel and dataType will always be the same for any given key value + * and are present here as a hint to how the value should be stored. + */ +- (BOOL)controller:(MTRDeviceController *)controller + storeValue:(id)value + forKey:(NSString *)key + securityLevel:(MTRStorageSecurityLevel)securityLevel + sharingType:(MTRStorageSharingType)sharingType; + +/** + * Remove the stored value for the given key. Returns whether the remove succeeded. + * + * securityLevel and dataType will always be the same for any given key value + * and are just present here to help locate the data if storage location is + * separated out by security level and data type. + */ +- (BOOL)controller:(MTRDeviceController *)controller + removeValueForKey:(NSString *)key + securityLevel:(MTRStorageSecurityLevel)securityLevel + sharingType:(MTRStorageSharingType)sharingType; +@end + +// TODO: FIXME: Is this a sane place to put this API? +MTR_EXTERN MTR_NEWLY_AVAILABLE NSSet * MTRDeviceControllerStorageClasses(void); + +NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIP/MTRDeviceController_Internal.h b/src/darwin/Framework/CHIP/MTRDeviceController_Internal.h index 28beb9b8244a13..89db3a9f951d3e 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceController_Internal.h +++ b/src/darwin/Framework/CHIP/MTRDeviceController_Internal.h @@ -29,6 +29,10 @@ #import "MTRBaseDevice.h" #import "MTRDeviceController.h" +#import "MTRDeviceControllerDataStore.h" +#import "MTRDeviceControllerStorageDelegate.h" + +#import @class MTRDeviceControllerStartupParamsInternal; @class MTRDeviceControllerFactory; @@ -73,12 +77,21 @@ NS_ASSUME_NONNULL_BEGIN */ @property (readonly, nullable) NSNumber * compressedFabricID; +/** + * The per-controller data store this controller was initialized with, if any. + */ +@property (nonatomic, nullable) MTRDeviceControllerDataStore * controllerDataStore; + /** * Init a newly created controller. * * Only MTRDeviceControllerFactory should be calling this. */ -- (instancetype)initWithFactory:(MTRDeviceControllerFactory *)factory queue:(dispatch_queue_t)queue; +- (instancetype)initWithFactory:(MTRDeviceControllerFactory *)factory + queue:(dispatch_queue_t)queue + storageDelegate:(id _Nullable)storageDelegate + storageDelegateQueue:(dispatch_queue_t _Nullable)storageDelegateQueue + uniqueIdentifier:(NSUUID *)uniqueIdentifier; /** * Check whether this controller is running on the given fabric, as represented diff --git a/src/darwin/Framework/CHIP/MTRSessionResumptionStorageBridge.h b/src/darwin/Framework/CHIP/MTRSessionResumptionStorageBridge.h new file mode 100644 index 00000000000000..389fcc2a9eba10 --- /dev/null +++ b/src/darwin/Framework/CHIP/MTRSessionResumptionStorageBridge.h @@ -0,0 +1,59 @@ +/** + * 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. + */ + +#import +#import + +#import "MTRDeviceControllerDataStore.h" + +#include + +NS_ASSUME_NONNULL_BEGIN + +/** + * Implements SessionResumptionStorage and dispatches the relevant lookup + * requests to running controllers as needed. + */ +class MTRSessionResumptionStorageBridge : public chip::SessionResumptionStorage +{ +public: + MTRSessionResumptionStorageBridge(MTRDeviceControllerFactory * factory); + + ~MTRSessionResumptionStorageBridge() {} + + // SessionResumptionStorage API. + CHIP_ERROR FindByScopedNodeId(const chip::ScopedNodeId & node, ResumptionIdStorage & resumptionId, + chip::Crypto::P256ECDHDerivedSecret & sharedSecret, chip::CATValues & peerCATs) override; + + CHIP_ERROR FindByResumptionId(ConstResumptionIdView resumptionId, chip::ScopedNodeId & node, + chip::Crypto::P256ECDHDerivedSecret & sharedSecret, chip::CATValues & peerCATs) override; + CHIP_ERROR Save(const chip::ScopedNodeId & node, ConstResumptionIdView resumptionId, + const chip::Crypto::P256ECDHDerivedSecret & sharedSecret, const chip::CATValues & peerCATs) override; + CHIP_ERROR DeleteAll(chip::FabricIndex fabricIndex) override; + +private: + /** + * Helper method to convert a MTRCASESessionResumptionInfo into the pieces + * we need to return from our Find* methods. + */ + static CHIP_ERROR DeconstructResumptionInfo(MTRCASESessionResumptionInfo * resumptionInfo, chip::NodeId & nodeID, + ResumptionIdStorage & resumptionId, + chip::Crypto::P256ECDHDerivedSecret & sharedSecret, chip::CATValues & peerCATs); + + MTRDeviceControllerFactory * mFactory; +}; + +NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIP/MTRSessionResumptionStorageBridge.mm b/src/darwin/Framework/CHIP/MTRSessionResumptionStorageBridge.mm new file mode 100644 index 00000000000000..56fe7bc7beb6f8 --- /dev/null +++ b/src/darwin/Framework/CHIP/MTRSessionResumptionStorageBridge.mm @@ -0,0 +1,149 @@ +/** + * 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. + */ + +#import "MTRSessionResumptionStorageBridge.h" + +#import "MTRConversion.h" +#import "MTRDeviceControllerFactory_Internal.h" +#import "MTRDeviceController_Internal.h" +#import "MTRLogging_Internal.h" +#import "NSDataSpanConversion.h" + +#include + +using namespace chip; + +MTRSessionResumptionStorageBridge::MTRSessionResumptionStorageBridge(MTRDeviceControllerFactory * factory) + : mFactory(factory) +{ +} + +CHIP_ERROR MTRSessionResumptionStorageBridge::FindByScopedNodeId(const chip::ScopedNodeId & node, + ResumptionIdStorage & resumptionId, chip::Crypto::P256ECDHDerivedSecret & sharedSecret, chip::CATValues & peerCATs) +{ + assertChipStackLockedByCurrentThread(); + + auto * controller = [mFactory runningControllerForFabricIndex:node.GetFabricIndex()]; + if (controller == nil) { + return CHIP_ERROR_KEY_NOT_FOUND; + } + + auto * resumptionInfo = [controller.controllerDataStore findResumptionInfoByNodeID:@(node.GetNodeId())]; + if (resumptionInfo == nil) { + return CHIP_ERROR_KEY_NOT_FOUND; + } + + NodeId ignored; + return DeconstructResumptionInfo(resumptionInfo, ignored, resumptionId, sharedSecret, peerCATs); +} + +CHIP_ERROR MTRSessionResumptionStorageBridge::FindByResumptionId(ConstResumptionIdView resumptionId, chip::ScopedNodeId & node, + chip::Crypto::P256ECDHDerivedSecret & sharedSecret, chip::CATValues & peerCATs) +{ + assertChipStackLockedByCurrentThread(); + + auto * resumptionIDData = AsData(resumptionId); + + auto * controllerList = [mFactory getRunningControllers]; + for (MTRDeviceController * controller in controllerList) { + FabricIndex fabricIndex = controller.fabricIndex; + if (!IsValidFabricIndex(fabricIndex)) { + // This controller is not sufficiently "running"; it does not have a + // fabric index yet. Just skip it. + continue; + } + + auto * resumptionInfo = [controller.controllerDataStore findResumptionInfoByResumptionID:resumptionIDData]; + if (resumptionInfo != nil) { + NodeId nodeID; + ResumptionIdStorage ignored; + ReturnErrorOnFailure(DeconstructResumptionInfo(resumptionInfo, nodeID, ignored, sharedSecret, peerCATs)); + node = ScopedNodeId(nodeID, fabricIndex); + return CHIP_NO_ERROR; + } + } + + // None of the controllers matched. + return CHIP_ERROR_KEY_NOT_FOUND; +} + +CHIP_ERROR MTRSessionResumptionStorageBridge::Save(const chip::ScopedNodeId & node, ConstResumptionIdView resumptionId, + const chip::Crypto::P256ECDHDerivedSecret & sharedSecret, const chip::CATValues & peerCATs) +{ + assertChipStackLockedByCurrentThread(); + + auto * controller = [mFactory runningControllerForFabricIndex:node.GetFabricIndex()]; + if (controller == nil) { + return CHIP_ERROR_PERSISTED_STORAGE_FAILED; + } + + auto * resumptionInfo = [[MTRCASESessionResumptionInfo alloc] init]; + resumptionInfo.nodeID = @(node.GetNodeId()); + resumptionInfo.resumptionID = AsData(resumptionId); + resumptionInfo.sharedSecret = AsData(ByteSpan(sharedSecret.ConstBytes(), sharedSecret.Length())); + resumptionInfo.caseAuthenticatedTags = CATValuesToSet(peerCATs); + + [controller.controllerDataStore storeResumptionInfo:resumptionInfo]; + return CHIP_NO_ERROR; +} + +CHIP_ERROR MTRSessionResumptionStorageBridge::DeleteAll(chip::FabricIndex fabricIndex) +{ + assertChipStackLockedByCurrentThread(); + + // NOTE: During controller startup, the Matter SDK thinks that the cert for + // a fabric index is changing and hence that it must remove session + // resumption data for that fabric index. For us that does not matter, + // since we don't key that data on fabric index anyway. But we do want to + // avoid doing this delete-on-startup so we can actually store session + // resumption data persistently. + + // And that is the only use of DeleteAll for controllers in practice, in the + // situations where we are using MTRSessionResumptionStorageBridge at all. + // So just no-op this function, but verify that our assumptions hold. + auto * controller = [mFactory runningControllerForFabricIndex:fabricIndex + includeControllerStartingUp:NO + includeControllerShuttingDown:YES]; + VerifyOrDieWithMsg(controller == nil, Controller, "Deleting resumption storage for controller outside startup"); + return CHIP_NO_ERROR; +} + +CHIP_ERROR MTRSessionResumptionStorageBridge::DeconstructResumptionInfo(MTRCASESessionResumptionInfo * resumptionInfo, + chip::NodeId & nodeID, ResumptionIdStorage & resumptionId, chip::Crypto::P256ECDHDerivedSecret & sharedSecret, + chip::CATValues & peerCATs) +{ + if (resumptionInfo.resumptionID.length != resumptionId.size()) { + MTR_LOG_ERROR("Unable to return resumption ID: Stored size %llu does not match required size %llu", + static_cast(resumptionInfo.resumptionID.length), + static_cast(resumptionId.size())); + return CHIP_ERROR_PERSISTED_STORAGE_FAILED; + } + + if (resumptionInfo.sharedSecret.length > sharedSecret.Capacity()) { + MTR_LOG_ERROR("Unable to return resumption shared secret: Stored size %llu is larger than allowed size %llu", + static_cast(resumptionInfo.sharedSecret.length), + static_cast(sharedSecret.Capacity())); + return CHIP_ERROR_PERSISTED_STORAGE_FAILED; + } + + nodeID = resumptionInfo.nodeID.unsignedLongLongValue; + memcpy(resumptionId.data(), resumptionInfo.resumptionID.bytes, resumptionInfo.resumptionID.length); + sharedSecret.SetLength(resumptionInfo.sharedSecret.length); + memcpy(sharedSecret.Bytes(), resumptionInfo.sharedSecret.bytes, resumptionInfo.sharedSecret.length); + ReturnErrorOnFailure(SetToCATValues(resumptionInfo.caseAuthenticatedTags, peerCATs)); + + return CHIP_NO_ERROR; +} diff --git a/src/darwin/Framework/CHIP/Matter.h b/src/darwin/Framework/CHIP/Matter.h index 9a5ce73864f2c2..26c74cd80487d2 100644 --- a/src/darwin/Framework/CHIP/Matter.h +++ b/src/darwin/Framework/CHIP/Matter.h @@ -40,7 +40,9 @@ #import #import #import +#import #import +#import #import #import #import diff --git a/src/darwin/Framework/CHIPTests/MTRControllerTests.m b/src/darwin/Framework/CHIPTests/MTRControllerTests.m index e7a18bcb13804f..63a5baf32b64d5 100644 --- a/src/darwin/Framework/CHIPTests/MTRControllerTests.m +++ b/src/darwin/Framework/CHIPTests/MTRControllerTests.m @@ -1524,8 +1524,6 @@ - (void)testControllerCATs [controller shutdown]; XCTAssertFalse([controller isRunning]); - fprintf(stderr, "DOING TOO LONG TEST\n"); - // // Trying to bring up the same fabric with too-long CATs should fail, if we // are taking the provided CATs into account. @@ -1536,8 +1534,6 @@ - (void)testControllerCATs controller = [factory createControllerOnExistingFabric:params error:nil]; XCTAssertNil(controller); - fprintf(stderr, "DOING INVALID TEST\n"); - // // Trying to bring up the same fabric with invalid CATs should fail, if we // are taking the provided CATs into account. @@ -1545,12 +1541,9 @@ - (void)testControllerCATs params.nodeID = @(17); params.operationalKeypair = operationalKeys; params.caseAuthenticatedTags = invalidCATs; - fprintf(stderr, "BRINGING UP CONTROLLER\n"); controller = [factory createControllerOnExistingFabric:params error:nil]; - fprintf(stderr, "CONTROLLER SHOULD BE NIL\n"); XCTAssertNil(controller); - fprintf(stderr, "STOPPING FACTORY\n"); [factory stopControllerFactory]; XCTAssertFalse([factory isRunning]); } diff --git a/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m b/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m new file mode 100644 index 00000000000000..fdf03eab12ba36 --- /dev/null +++ b/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m @@ -0,0 +1,1124 @@ +/* + * 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. + */ + +#import + +// system dependencies +#import + +#import "MTRErrorTestUtils.h" +#import "MTRFabricInfoChecker.h" +#import "MTRTestKeys.h" +#import "MTRTestResetCommissioneeHelper.h" +#import "MTRTestStorage.h" + +static const uint16_t kPairingTimeoutInSeconds = 10; +static const uint16_t kTimeoutInSeconds = 3; +static NSString * kOnboardingPayload = @"MT:-24J0AFN00KA0648G00"; +static const uint16_t kLocalPort = 5541; +static const uint16_t kTestVendorId = 0xFFF1u; + +@interface MTRPerControllerStorageTestsControllerDelegate : NSObject +@property (nonatomic, strong) XCTestExpectation * expectation; +@property (nonatomic, strong) NSNumber * deviceID; +@end + +@implementation MTRPerControllerStorageTestsControllerDelegate +- (id)initWithExpectation:(XCTestExpectation *)expectation newNodeID:(NSNumber *)newNodeID +{ + self = [super init]; + if (self) { + _expectation = expectation; + _deviceID = newNodeID; + } + return self; +} + +- (void)controller:(MTRDeviceController *)controller commissioningSessionEstablishmentDone:(NSError *)error +{ + XCTAssertEqual(error.code, 0); + + __auto_type * params = [[MTRCommissioningParameters alloc] init]; + + NSError * commissionError = nil; + [controller commissionNodeWithID:self.deviceID commissioningParams:params error:&commissionError]; + XCTAssertNil(commissionError); + + // Keep waiting for controller:commissioningComplete: +} + +- (void)controller:(MTRDeviceController *)controller commissioningComplete:(NSError *)error +{ + XCTAssertEqual(error.code, 0); + [_expectation fulfill]; + _expectation = nil; +} + +@end + +@interface MTRPerControllerStorageTestsStorageDelegate : NSObject +@property (nonatomic, readonly) NSMutableDictionary * storage; +@property (nonatomic, readonly) NSUUID * controllerID; +@end + +@implementation MTRPerControllerStorageTestsStorageDelegate + +- (instancetype)initWithControllerID:(NSUUID *)controllerID +{ + if (!(self = [super init])) { + return nil; + } + + _storage = [[NSMutableDictionary alloc] init]; + _controllerID = controllerID; + return self; +} + +- (nullable id)controller:(MTRDeviceController *)controller + valueForKey:(NSString *)key + securityLevel:(MTRStorageSecurityLevel)securityLevel + sharingType:(MTRStorageSharingType)sharingType +{ + XCTAssertEqualObjects(_controllerID, controller.uniqueIdentifier); + + __auto_type * data = self.storage[key]; + if (data == nil) { + return data; + } + + NSError * error; + id value = [NSKeyedUnarchiver unarchivedObjectOfClasses:MTRDeviceControllerStorageClasses() fromData:data error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(data); + + return value; +} + +- (BOOL)controller:(MTRDeviceController *)controller + storeValue:(id)value + forKey:(NSString *)key + securityLevel:(MTRStorageSecurityLevel)securityLevel + sharingType:(MTRStorageSharingType)sharingType +{ + XCTAssertEqualObjects(_controllerID, controller.uniqueIdentifier); + + NSError * error; + NSData * data = [NSKeyedArchiver archivedDataWithRootObject:value requiringSecureCoding:YES error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(data); + + self.storage[key] = data; + return YES; +} + +- (BOOL)controller:(MTRDeviceController *)controller + removeValueForKey:(NSString *)key + securityLevel:(MTRStorageSecurityLevel)securityLevel + sharingType:(MTRStorageSharingType)sharingType +{ + XCTAssertEqualObjects(_controllerID, controller.uniqueIdentifier); + self.storage[key] = nil; + return YES; +} + +@end + +@interface MTRPerControllerStorageTestsCertificateIssuer : NSObject +- (instancetype)initWithRootCertificate:(MTRCertificateDERBytes)rootCertificate + intermediateCertificate:(MTRCertificateDERBytes _Nullable)intermediateCertificate + signingKey:(id)signingKey + fabricID:(NSNumber *)fabricID; + +@property (nonatomic, readonly) MTRCertificateDERBytes rootCertificate; +@property (nonatomic, readonly, nullable) MTRCertificateDERBytes intermediateCertificate; +@property (nonatomic, readonly) id signingKey; +@property (nonatomic, readonly) NSNumber * fabricID; + +// The node ID to use for the next operational certificate we issue. This will +// be set to null after every certificate issuance. +@property (nonatomic, nullable) NSNumber * nextNodeID; + +@property (nonatomic, readonly) BOOL shouldSkipAttestationCertificateValidation; + +@end + +@implementation MTRPerControllerStorageTestsCertificateIssuer + +- (instancetype)initWithRootCertificate:(MTRCertificateDERBytes)rootCertificate + intermediateCertificate:(MTRCertificateDERBytes _Nullable)intermediateCertificate + signingKey:(id)signingKey + fabricID:(NSNumber *)fabricID +{ + if (!(self = [super init])) { + return nil; + } + + _rootCertificate = rootCertificate; + _intermediateCertificate = intermediateCertificate; + _signingKey = signingKey; + _fabricID = fabricID; + _nextNodeID = nil; + _shouldSkipAttestationCertificateValidation = NO; + + return self; +} + +- (void)issueOperationalCertificateForRequest:(MTROperationalCSRInfo *)csrInfo + attestationInfo:(MTRDeviceAttestationInfo *)attestationInfo + controller:(MTRDeviceController *)controller + completion:(void (^)(MTROperationalCertificateChain * _Nullable info, + NSError * _Nullable error))completion +{ + if (self.nextNodeID == nil) { + completion(nil, [NSError errorWithDomain:@"TestError" code:0 userInfo:@{ @"reason" : @"nextNodeID is nil" }]); + return; + } + + MTRCertificateDERBytes signingCertificate; + if (self.intermediateCertificate != nil) { + signingCertificate = self.intermediateCertificate; + } else { + signingCertificate = self.rootCertificate; + } + + __auto_type * csr = csrInfo.csr; + XCTAssertNotNil(csr); + + NSError * error; + __auto_type * rawPublicKey = [MTRCertificates publicKeyFromCSR:csr error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(rawPublicKey); + + if (error != nil) { + completion(nil, error); + return; + } + + NSDictionary * attributes = @{ + (__bridge NSString *) kSecAttrKeyType : (__bridge NSString *) kSecAttrKeyTypeECSECPrimeRandom, + (__bridge NSString *) kSecAttrKeyClass : (__bridge NSString *) kSecAttrKeyClassPublic + }; + CFErrorRef keyCreationError = NULL; + SecKeyRef publicKey + = SecKeyCreateWithData((__bridge CFDataRef) rawPublicKey, (__bridge CFDictionaryRef) attributes, &keyCreationError); + XCTAssertNil((__bridge id) keyCreationError); + XCTAssertNotNil((__bridge id) publicKey); + + __auto_type * operationalCert = [MTRCertificates createOperationalCertificate:self.signingKey + signingCertificate:signingCertificate + operationalPublicKey:publicKey + fabricID:self.fabricID + nodeID:self.nextNodeID + caseAuthenticatedTags:nil + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(operationalCert); + + if (error != nil) { + completion(nil, error); + return; + } + + __auto_type * certChain = [[MTROperationalCertificateChain alloc] initWithOperationalCertificate:operationalCert + intermediateCertificate:self.intermediateCertificate + rootCertificate:self.rootCertificate + adminSubject:nil]; + completion(certChain, nil); +} + +@end + +@interface MTRPerControllerStorageTests : XCTestCase +@end + +@implementation MTRPerControllerStorageTests { + dispatch_queue_t _storageQueue; +} + ++ (void)tearDown +{ +} + +- (void)setUp +{ + // Per-test setup, runs before each test. + [super setUp]; + [self setContinueAfterFailure:NO]; + + _storageQueue = dispatch_queue_create("test.storage.queue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL); + + [self startFactory]; +} + +- (void)tearDown +{ + // Per-test teardown, runs after each test. + [self stopFactory]; + _storageQueue = nil; + [super tearDown]; +} + +- (void)startFactory +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type * factoryParams = [[MTRDeviceControllerFactoryParams alloc] init]; + factoryParams.port = @(kLocalPort); + + NSError * error; + BOOL ok = [factory startControllerFactory:factoryParams error:&error]; + XCTAssertNil(error); + XCTAssertTrue(ok); +} + +- (void)stopFactory +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + [factory stopControllerFactory]; + XCTAssertFalse(factory.isRunning); +} + +// Test helpers + +- (void)commissionWithController:(MTRDeviceController *)controller newNodeID:(NSNumber *)newNodeID +{ + XCTestExpectation * expectation = [self expectationWithDescription:@"Pairing Complete"]; + + __auto_type * deviceControllerDelegate = [[MTRPerControllerStorageTestsControllerDelegate alloc] initWithExpectation:expectation + newNodeID:newNodeID]; + dispatch_queue_t callbackQueue = dispatch_queue_create("com.chip.device_controller_delegate", DISPATCH_QUEUE_SERIAL); + + [controller setDeviceControllerDelegate:deviceControllerDelegate queue:callbackQueue]; + + NSError * error; + __auto_type * payload = [MTRSetupPayload setupPayloadWithOnboardingPayload:kOnboardingPayload error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(payload); + + [controller setupCommissioningSessionWithPayload:payload newNodeID:newNodeID error:&error]; + XCTAssertNil(error); + + [self waitForExpectations:@[ expectation ] timeout:kPairingTimeoutInSeconds]; +} + +- (nullable MTRDeviceController *)startControllerWithRootKeys:(MTRTestKeys *)rootKeys + operationalKeys:(MTRTestKeys *)operationalKeys + fabricID:(NSNumber *)fabricID + nodeID:(NSNumber *)nodeID + storage:(MTRPerControllerStorageTestsStorageDelegate *)storage + caseAuthenticatedTags:(nullable NSSet *)caseAuthenticatedTags + error:(NSError * __autoreleasing *)error + certificateIssuer: + (MTRPerControllerStorageTestsCertificateIssuer * __autoreleasing *)certificateIssuer +{ + XCTAssertTrue(error != NULL); + + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + // Specify a fixed issuerID, so we get the same cert if we use the same keys. + __auto_type * root = [MTRCertificates createRootCertificate:rootKeys issuerID:@(1) fabricID:nil error:error]; + XCTAssertNil(*error); + XCTAssertNotNil(root); + + __auto_type * operational = [MTRCertificates createOperationalCertificate:rootKeys + signingCertificate:root + operationalPublicKey:operationalKeys.publicKey + fabricID:fabricID + nodeID:nodeID + caseAuthenticatedTags:caseAuthenticatedTags + error:error]; + XCTAssertNil(*error); + XCTAssertNotNil(operational); + + __auto_type * params = + [[MTRDeviceControllerExternalCertificateStartupParameters alloc] initWithStorageDelegate:storage + storageDelegateQueue:_storageQueue + uniqueIdentifier:storage.controllerID + ipk:rootKeys.ipk + vendorID:@(kTestVendorId) + operationalKeypair:operationalKeys + operationalCertificate:operational + intermediateCertificate:nil + rootCertificate:root]; + XCTAssertNotNil(params); + + __auto_type * ourCertificateIssuer = [[MTRPerControllerStorageTestsCertificateIssuer alloc] initWithRootCertificate:root + intermediateCertificate:nil + signingKey:rootKeys + fabricID:fabricID]; + XCTAssertNotNil(ourCertificateIssuer); + + if (certificateIssuer) { + *certificateIssuer = ourCertificateIssuer; + } + + [params setOperationalCertificateIssuer:ourCertificateIssuer queue:dispatch_get_main_queue()]; + + return [factory createController:params error:error]; +} + +- (nullable MTRDeviceController *)startControllerWithRootKeys:(MTRTestKeys *)rootKeys + operationalKeys:(MTRTestKeys *)operationalKeys + fabricID:(NSNumber *)fabricID + nodeID:(NSNumber *)nodeID + storage:(MTRPerControllerStorageTestsStorageDelegate *)storage + error:(NSError * __autoreleasing *)error + certificateIssuer: + (MTRPerControllerStorageTestsCertificateIssuer * __autoreleasing *)certificateIssuer +{ + return [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID + storage:storage + caseAuthenticatedTags:nil + error:error + certificateIssuer:certificateIssuer]; +} + +- (nullable MTRDeviceController *)startControllerWithRootKeys:(MTRTestKeys *)rootKeys + operationalKeys:(MTRTestKeys *)operationalKeys + fabricID:(NSNumber *)fabricID + nodeID:(NSNumber *)nodeID + storage:(MTRPerControllerStorageTestsStorageDelegate *)storage + caseAuthenticatedTags:(nullable NSSet *)caseAuthenticatedTags + error:(NSError * __autoreleasing *)error +{ + return [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID + storage:storage + caseAuthenticatedTags:caseAuthenticatedTags + error:error + certificateIssuer:nil]; +} + +- (nullable MTRDeviceController *)startControllerWithRootKeys:(MTRTestKeys *)rootKeys + operationalKeys:(MTRTestKeys *)operationalKeys + fabricID:(NSNumber *)fabricID + nodeID:(NSNumber *)nodeID + storage:(MTRPerControllerStorageTestsStorageDelegate *)storage + error:(NSError * __autoreleasing *)error +{ + return [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID + storage:storage + error:error + certificateIssuer:nil]; +} + +- (void)test001_BasicControllerStartup +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type * rootKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(rootKeys); + + __auto_type * operationalKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(operationalKeys); + + __auto_type * storageDelegate = [[MTRPerControllerStorageTestsStorageDelegate alloc] initWithControllerID:[NSUUID UUID]]; + + NSNumber * nodeID = @(123); + NSNumber * fabricID = @(456); + + NSError * error; + MTRDeviceController * controller = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID + storage:storageDelegate + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(controller); + XCTAssertTrue([controller isRunning]); + + XCTAssertEqualObjects(controller.controllerNodeID, nodeID); + + // This was the first controller we brought up, so it should have come up + // with fabric index 1. + __auto_type * fabricInfoList = factory.knownFabrics; + CheckFabricInfo(fabricInfoList, [NSMutableSet setWithArray:@[ + @{ + @"rootPublicKey" : rootKeys.publicKeyData, + @"vendorID" : @(kTestVendorId), + @"fabricID" : fabricID, + @"nodeID" : nodeID, + @"label" : @"", + @"hasIntermediateCertificate" : @(NO), + @"fabricIndex" : @(1), + }, + ]]); + + [controller shutdown]; + XCTAssertFalse([controller isRunning]); +} + +- (void)test002_TryStartingTwoControllersWithSameNodeID +{ + __auto_type * rootKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(rootKeys); + + __auto_type * operationalKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(operationalKeys); + + __auto_type * storageDelegate = [[MTRPerControllerStorageTestsStorageDelegate alloc] initWithControllerID:[NSUUID UUID]]; + + NSNumber * nodeID = @(123); + NSNumber * fabricID = @(456); + + NSError * error; + MTRDeviceController * controller = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID + storage:storageDelegate + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(controller); + XCTAssertTrue([controller isRunning]); + + XCTAssertEqualObjects(controller.controllerNodeID, nodeID); + + // Try to bring up another controller with the same identity. This should + // fail, since our controller is still running + MTRDeviceController * otherController = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID + storage:storageDelegate + error:&error]; + XCTAssertNil(otherController); + XCTAssertNotNil(error); + + // Our controller should still be running. + XCTAssertTrue([controller isRunning]); + + [controller shutdown]; + XCTAssertFalse([controller isRunning]); +} + +- (void)test003_TestTwoControllersSameUUID +{ + __auto_type * rootKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(rootKeys); + + __auto_type * operationalKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(operationalKeys); + XCTAssertEqual(operationalKeys.signatureCount, 0); + + __auto_type * storageDelegate = [[MTRPerControllerStorageTestsStorageDelegate alloc] initWithControllerID:[NSUUID UUID]]; + + NSNumber * fabricID = @(456); + + NSNumber * nodeID1 = @(123); + + NSError * error; + MTRDeviceController * controller = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID1 + storage:storageDelegate + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(controller); + XCTAssertTrue([controller isRunning]); + + XCTAssertEqualObjects(controller.controllerNodeID, nodeID1); + + // Try to bring up another controller with the same uniqueIdentifier (but a different + // node identity). This should fail, since our controller is still running. + NSNumber * nodeID2 = @(789); + MTRDeviceController * otherController = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID2 + storage:storageDelegate + error:&error]; + XCTAssertNil(otherController); + XCTAssertNotNil(error); + + // Our controller should still be running. + XCTAssertTrue([controller isRunning]); + + [controller shutdown]; + XCTAssertFalse([controller isRunning]); +} + +- (void)test004_TestBasicSessionResumption +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type queue = dispatch_get_main_queue(); + + __auto_type * rootKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(rootKeys); + + __auto_type * operationalKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(operationalKeys); + XCTAssertEqual(operationalKeys.signatureCount, 0); + + __auto_type * storageDelegate = [[MTRPerControllerStorageTestsStorageDelegate alloc] initWithControllerID:[NSUUID UUID]]; + + NSNumber * nodeID = @(123); + NSNumber * fabricID = @(456); + + NSError * error; + MTRPerControllerStorageTestsCertificateIssuer * certificateIssuer; + MTRDeviceController * controller = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID + storage:storageDelegate + error:&error + certificateIssuer:&certificateIssuer]; + XCTAssertNil(error); + XCTAssertNotNil(controller); + XCTAssertTrue([controller isRunning]); + + XCTAssertEqualObjects(controller.controllerNodeID, nodeID); + + // Now commission the device, to test that that works. + NSNumber * deviceID = @(17); + certificateIssuer.nextNodeID = deviceID; + [self commissionWithController:controller newNodeID:deviceID]; + + // We should have established CASE using our operational key. + XCTAssertEqual(operationalKeys.signatureCount, 1); + + [controller shutdown]; + XCTAssertFalse([controller isRunning]); + + // Start the controller again using the same identity. This should work, + // because we cleared out the fabric info when the controller shut down. + controller = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID + storage:storageDelegate + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(controller); + XCTAssertTrue([controller isRunning]); + + XCTAssertEqualObjects(controller.controllerNodeID, nodeID); + + // Now we should have come up with fabric index 2. + __auto_type * fabricInfoList = factory.knownFabrics; + CheckFabricInfo(fabricInfoList, [NSMutableSet setWithArray:@[ + @{ + @"rootPublicKey" : rootKeys.publicKeyData, + @"vendorID" : @(kTestVendorId), + @"fabricID" : fabricID, + @"nodeID" : nodeID, + @"label" : @"", + @"hasIntermediateCertificate" : @(NO), + @"fabricIndex" : @(2), + }, + ]]); + + // Try sending an attribute read and make sure it works. + __auto_type * device = [MTRBaseDevice deviceWithNodeID:deviceID controller:controller]; + __auto_type * onOffCluster = [[MTRBaseClusterOnOff alloc] initWithDevice:device endpointID:@(1) queue:queue]; + __auto_type * readExpectation = [self expectationWithDescription:@"Read OnOff attribute"]; + [onOffCluster readAttributeOnOffWithCompletion:^(NSNumber * value, NSError * _Nullable error) { + XCTAssertNil(error); + // We expect the device to be off. + XCTAssertEqualObjects(value, @(0)); + [readExpectation fulfill]; + }]; + + [self waitForExpectations:@[ readExpectation ] timeout:kTimeoutInSeconds]; + + // We should have done CASE resumption, so not done any new signing using + // our keys. + XCTAssertEqual(operationalKeys.signatureCount, 1); + + // Reset our commissionee. + ResetCommissionee(device, queue, self, kTimeoutInSeconds); + + [controller shutdown]; + XCTAssertFalse([controller isRunning]); +} + +- (void)test005_TestSessionResumptionDataClearingNodeIDChanged +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type queue = dispatch_get_main_queue(); + + __auto_type * rootKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(rootKeys); + + __auto_type * operationalKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(operationalKeys); + XCTAssertEqual(operationalKeys.signatureCount, 0); + + __auto_type * storageDelegate = [[MTRPerControllerStorageTestsStorageDelegate alloc] initWithControllerID:[NSUUID UUID]]; + + NSNumber * nodeID1 = @(123); + NSNumber * nodeID2 = @(246); + NSNumber * fabricID = @(456); + + NSError * error; + MTRPerControllerStorageTestsCertificateIssuer * certificateIssuer; + MTRDeviceController * controller = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID1 + storage:storageDelegate + error:&error + certificateIssuer:&certificateIssuer]; + XCTAssertNil(error); + XCTAssertNotNil(controller); + XCTAssertTrue([controller isRunning]); + + XCTAssertEqualObjects(controller.controllerNodeID, nodeID1); + + NSNumber * deviceID = @(17); + certificateIssuer.nextNodeID = deviceID; + [self commissionWithController:controller newNodeID:deviceID]; + + // We should have established CASE using our operational key. + XCTAssertEqual(operationalKeys.signatureCount, 1); + + __auto_type * device1 = [MTRBaseDevice deviceWithNodeID:deviceID controller:controller]; + + // Now change ACLs so that nodeID2 has access and nodeID1 does not. + __auto_type * admin = [[MTRAccessControlClusterAccessControlEntryStruct alloc] init]; + admin.privilege = @(MTRAccessControlEntryPrivilegeAdminister); + admin.authMode = @(MTRAccessControlEntryAuthModeCASE); + admin.subjects = @[ nodeID2 ]; + + __auto_type * aclCluster = [[MTRBaseClusterAccessControl alloc] initWithDevice:device1 endpointID:@(0) queue:queue]; + + XCTestExpectation * aclWriteExpectation = [self expectationWithDescription:@"ACLs changed so new node ID can administer"]; + [aclCluster writeAttributeACLWithValue:@[ admin ] + completion:^(NSError * _Nullable err) { + XCTAssertNil(err); + [aclWriteExpectation fulfill]; + }]; + + [self waitForExpectations:@[ aclWriteExpectation ] timeout:kTimeoutInSeconds]; + + [controller shutdown]; + XCTAssertFalse([controller isRunning]); + + // There should have been no more CASE establishment going on. + XCTAssertEqual(operationalKeys.signatureCount, 1); + + // Bring up a controller with the same storage and keys and so on but nodeID2. + controller = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID2 + storage:storageDelegate + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(controller); + XCTAssertTrue([controller isRunning]); + + XCTAssertEqualObjects(controller.controllerNodeID, nodeID2); + + // Try sending an attribute read and make sure it works. + __auto_type * device2 = [MTRBaseDevice deviceWithNodeID:deviceID controller:controller]; + __auto_type * onOffCluster = [[MTRBaseClusterOnOff alloc] initWithDevice:device2 endpointID:@(1) queue:queue]; + __auto_type * readExpectation = [self expectationWithDescription:@"Read OnOff attribute"]; + [onOffCluster readAttributeOnOffWithCompletion:^(NSNumber * value, NSError * _Nullable error) { + XCTAssertNil(error); + // We expect the device to be off. + XCTAssertEqualObjects(value, @(0)); + [readExpectation fulfill]; + }]; + + [self waitForExpectations:@[ readExpectation ] timeout:kTimeoutInSeconds]; + + // We should note have done CASE resumption, since our identity changed. + XCTAssertEqual(operationalKeys.signatureCount, 2); + + // Reset our commissionee. + ResetCommissionee(device2, queue, self, kTimeoutInSeconds); + + [controller shutdown]; + XCTAssertFalse([controller isRunning]); +} + +- (void)test006_TestSessionResumptionDataClearingCATsChanged +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type queue = dispatch_get_main_queue(); + + __auto_type * rootKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(rootKeys); + + __auto_type * operationalKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(operationalKeys); + XCTAssertEqual(operationalKeys.signatureCount, 0); + + __auto_type * storageDelegate = [[MTRPerControllerStorageTestsStorageDelegate alloc] initWithControllerID:[NSUUID UUID]]; + + NSNumber * nodeID = @(123); + NSNumber * fabricID = @(456); + + NSError * error; + MTRPerControllerStorageTestsCertificateIssuer * certificateIssuer; + MTRDeviceController * controller = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID + storage:storageDelegate + error:&error + certificateIssuer:&certificateIssuer]; + XCTAssertNil(error); + XCTAssertNotNil(controller); + XCTAssertTrue([controller isRunning]); + + XCTAssertEqualObjects(controller.controllerNodeID, nodeID); + + NSNumber * deviceID = @(17); + certificateIssuer.nextNodeID = deviceID; + [self commissionWithController:controller newNodeID:deviceID]; + + // We should have established CASE using our operational key. + XCTAssertEqual(operationalKeys.signatureCount, 1); + + __auto_type * device1 = [MTRBaseDevice deviceWithNodeID:deviceID controller:controller]; + + // Now change ACLs so that CAT 0x12340001 has access and nodeID does not. + uint32_t cat = 0x12340001; + NSNumber * catSubject = @(0xFFFFFFFD00000000 | cat); + __auto_type * admin = [[MTRAccessControlClusterAccessControlEntryStruct alloc] init]; + admin.privilege = @(MTRAccessControlEntryPrivilegeAdminister); + admin.authMode = @(MTRAccessControlEntryAuthModeCASE); + admin.subjects = @[ catSubject ]; + + __auto_type * aclCluster = [[MTRBaseClusterAccessControl alloc] initWithDevice:device1 endpointID:@(0) queue:queue]; + + XCTestExpectation * aclWriteExpectation = [self expectationWithDescription:@"ACLs changed so new node ID can administer"]; + [aclCluster writeAttributeACLWithValue:@[ admin ] + completion:^(NSError * _Nullable err) { + XCTAssertNil(err); + [aclWriteExpectation fulfill]; + }]; + + [self waitForExpectations:@[ aclWriteExpectation ] timeout:kTimeoutInSeconds]; + + [controller shutdown]; + XCTAssertFalse([controller isRunning]); + + // There should have been no more CASE establishment going on. + XCTAssertEqual(operationalKeys.signatureCount, 1); + + // Bring up a controller with the same storage and keys and so on but using + // our new CAT + controller = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID + nodeID:nodeID + storage:storageDelegate + caseAuthenticatedTags:[NSSet setWithArray:@[ @(cat) ]] + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(controller); + XCTAssertTrue([controller isRunning]); + + XCTAssertEqualObjects(controller.controllerNodeID, nodeID); + + // Try sending an attribute read and make sure it works. + __auto_type * device2 = [MTRBaseDevice deviceWithNodeID:deviceID controller:controller]; + __auto_type * onOffCluster = [[MTRBaseClusterOnOff alloc] initWithDevice:device2 endpointID:@(1) queue:queue]; + __auto_type * readExpectation = [self expectationWithDescription:@"Read OnOff attribute"]; + [onOffCluster readAttributeOnOffWithCompletion:^(NSNumber * value, NSError * _Nullable error) { + XCTAssertNil(error); + // We expect the device to be off. + XCTAssertEqualObjects(value, @(0)); + [readExpectation fulfill]; + }]; + + [self waitForExpectations:@[ readExpectation ] timeout:kTimeoutInSeconds]; + + // We should note have done CASE resumption, since our CATs changed. + XCTAssertEqual(operationalKeys.signatureCount, 2); + + // Reset our commissionee. + ResetCommissionee(device2, queue, self, kTimeoutInSeconds); + + [controller shutdown]; + XCTAssertFalse([controller isRunning]); +} + +- (void)test007_TestMultipleControllers +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type * rootKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(rootKeys); + + __auto_type * operationalKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(operationalKeys); + XCTAssertEqual(operationalKeys.signatureCount, 0); + + NSNumber * nodeID1 = @(123); + NSNumber * nodeID2 = @(456); + NSNumber * fabricID1 = @(1); + NSNumber * fabricID2 = @(2); + + __auto_type * storageDelegate1 = [[MTRPerControllerStorageTestsStorageDelegate alloc] initWithControllerID:[NSUUID UUID]]; + + // Start several controllers that have distinct identities but share some + // node/fabric IDs. + NSError * error; + MTRPerControllerStorageTestsCertificateIssuer * certificateIssuer; + MTRDeviceController * controller1 = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID1 + nodeID:nodeID1 + storage:storageDelegate1 + error:&error + certificateIssuer:&certificateIssuer]; + XCTAssertNil(error); + XCTAssertNotNil(controller1); + XCTAssertTrue([controller1 isRunning]); + + __auto_type * storageDelegate2 = [[MTRPerControllerStorageTestsStorageDelegate alloc] initWithControllerID:[NSUUID UUID]]; + MTRDeviceController * controller2 = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID1 + nodeID:nodeID2 + storage:storageDelegate2 + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(controller2); + XCTAssertTrue([controller2 isRunning]); + + __auto_type * storageDelegate3 = [[MTRPerControllerStorageTestsStorageDelegate alloc] initWithControllerID:[NSUUID UUID]]; + MTRDeviceController * controller3 = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID2 + nodeID:nodeID1 + storage:storageDelegate3 + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(controller3); + XCTAssertTrue([controller3 isRunning]); + + // Now check our fabric table + __auto_type * fabricInfoList = factory.knownFabrics; + CheckFabricInfo(fabricInfoList, [NSMutableSet setWithArray:@[ + @{ + @"rootPublicKey" : rootKeys.publicKeyData, + @"vendorID" : @(kTestVendorId), + @"fabricID" : fabricID1, + @"nodeID" : nodeID1, + @"label" : @"", + @"hasIntermediateCertificate" : @(NO), + @"fabricIndex" : @(1), + }, + @{ + @"rootPublicKey" : rootKeys.publicKeyData, + @"vendorID" : @(kTestVendorId), + @"fabricID" : fabricID1, + @"nodeID" : nodeID2, + @"label" : @"", + @"hasIntermediateCertificate" : @(NO), + @"fabricIndex" : @(2), + }, + @{ + @"rootPublicKey" : rootKeys.publicKeyData, + @"vendorID" : @(kTestVendorId), + @"fabricID" : fabricID2, + @"nodeID" : nodeID1, + @"label" : @"", + @"hasIntermediateCertificate" : @(NO), + @"fabricIndex" : @(3), + }, + ]]); + + // Restart controller2 + [controller2 shutdown]; + XCTAssertFalse([controller2 isRunning]); + controller2 = [self startControllerWithRootKeys:rootKeys + operationalKeys:operationalKeys + fabricID:fabricID1 + nodeID:nodeID2 + storage:storageDelegate2 + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(controller2); + XCTAssertTrue([controller2 isRunning]); + + // Now check our fabric table again. + fabricInfoList = factory.knownFabrics; + CheckFabricInfo(fabricInfoList, [NSMutableSet setWithArray:@[ + @{ + @"rootPublicKey" : rootKeys.publicKeyData, + @"vendorID" : @(kTestVendorId), + @"fabricID" : fabricID1, + @"nodeID" : nodeID1, + @"label" : @"", + @"hasIntermediateCertificate" : @(NO), + @"fabricIndex" : @(1), + }, + @{ + @"rootPublicKey" : rootKeys.publicKeyData, + @"vendorID" : @(kTestVendorId), + @"fabricID" : fabricID1, + @"nodeID" : nodeID2, + @"label" : @"", + @"hasIntermediateCertificate" : @(NO), + @"fabricIndex" : @(4), + }, + @{ + @"rootPublicKey" : rootKeys.publicKeyData, + @"vendorID" : @(kTestVendorId), + @"fabricID" : fabricID2, + @"nodeID" : nodeID1, + @"label" : @"", + @"hasIntermediateCertificate" : @(NO), + @"fabricIndex" : @(3), + }, + ]]); + + // Now commission the device from controller1 + NSNumber * deviceID = @(17); + certificateIssuer.nextNodeID = deviceID; + [self commissionWithController:controller1 newNodeID:deviceID]; + + // We should have established CASE using our operational key. + XCTAssertEqual(operationalKeys.signatureCount, 1); + + // Ensure that controller2 does not have the same node ID as controller1. + XCTAssertNotEqualObjects(controller1.controllerNodeID, controller2.controllerNodeID); + + __auto_type * device1 = [MTRBaseDevice deviceWithNodeID:deviceID controller:controller1]; + __auto_type * device2 = [MTRBaseDevice deviceWithNodeID:deviceID controller:controller2]; + + dispatch_queue_t queue = dispatch_get_main_queue(); + __auto_type * onOff1 = [[MTRBaseClusterOnOff alloc] initWithDevice:device1 endpointID:@(1) queue:queue]; + __auto_type * onOff2 = [[MTRBaseClusterOnOff alloc] initWithDevice:device2 endpointID:@(1) queue:queue]; + + // Check that device1 can read the On/Off attribute + XCTestExpectation * canReadExpectation1 = [self expectationWithDescription:@"Initial commissioner can read on/off"]; + [onOff1 readAttributeOnOffWithCompletion:^(NSNumber * _Nullable value, NSError * _Nullable err) { + XCTAssertNil(err); + XCTAssertEqualObjects(value, @(0)); + [canReadExpectation1 fulfill]; + }]; + + [self waitForExpectations:@[ canReadExpectation1 ] timeout:kTimeoutInSeconds]; + + // Check that device2 cannot read the On/Off attribute due to missing ACLs. + XCTestExpectation * cantReadExpectation1 = [self expectationWithDescription:@"New node can't read on/off yet"]; + [onOff2 readAttributeOnOffWithCompletion:^(NSNumber * _Nullable value, NSError * _Nullable err) { + XCTAssertNil(value); + XCTAssertNotNil(err); + XCTAssertEqual([MTRErrorTestUtils errorToZCLErrorCode:err], MTRInteractionErrorCodeUnsupportedAccess); + [cantReadExpectation1 fulfill]; + }]; + + [self waitForExpectations:@[ cantReadExpectation1 ] timeout:kTimeoutInSeconds]; + + // Now change ACLs so that device2 can read. + __auto_type * admin1 = [[MTRAccessControlClusterAccessControlEntryStruct alloc] init]; + admin1.privilege = @(MTRAccessControlEntryPrivilegeAdminister); + admin1.authMode = @(MTRAccessControlEntryAuthModeCASE); + admin1.subjects = @[ controller1.controllerNodeID ]; + + __auto_type * admin2 = [[MTRAccessControlClusterAccessControlEntryStruct alloc] init]; + admin2.privilege = @(MTRAccessControlEntryPrivilegeAdminister); + admin2.authMode = @(MTRAccessControlEntryAuthModeCASE); + admin2.subjects = @[ controller2.controllerNodeID ]; + + __auto_type * acl1 = [[MTRBaseClusterAccessControl alloc] initWithDevice:device1 endpointID:@(0) queue:queue]; + + XCTestExpectation * let2ReadExpectation = [self expectationWithDescription:@"ACLs changed so new node can read"]; + [acl1 writeAttributeACLWithValue:@[ admin1, admin2 ] + completion:^(NSError * _Nullable err) { + XCTAssertNil(err); + [let2ReadExpectation fulfill]; + }]; + + [self waitForExpectations:@[ let2ReadExpectation ] timeout:kTimeoutInSeconds]; + + // Check that device2 can read the On/Off attribute + XCTestExpectation * canReadExpectation2 = [self expectationWithDescription:@"New node can read on/off"]; + [onOff2 readAttributeOnOffWithCompletion:^(NSNumber * _Nullable value, NSError * _Nullable err) { + XCTAssertNil(err); + XCTAssertEqualObjects(value, @(0)); + [canReadExpectation2 fulfill]; + }]; + + [self waitForExpectations:@[ canReadExpectation2 ] timeout:kTimeoutInSeconds]; + + // Check that device1 can still read the On/Off attribute + XCTestExpectation * canReadExpectation3 = [self expectationWithDescription:@"Initial commissioner can still read on/off"]; + [onOff1 readAttributeOnOffWithCompletion:^(NSNumber * _Nullable value, NSError * _Nullable err) { + XCTAssertNil(err); + XCTAssertEqualObjects(value, @(0)); + [canReadExpectation3 fulfill]; + }]; + + [self waitForExpectations:@[ canReadExpectation3 ] timeout:kTimeoutInSeconds]; + + // Check that the two devices are running on the same fabric. + __auto_type * opCreds1 = [[MTRBaseClusterOperationalCredentials alloc] initWithDevice:device1 endpoint:0 queue:queue]; + __auto_type * opCreds2 = [[MTRBaseClusterOperationalCredentials alloc] initWithDevice:device2 endpoint:0 queue:queue]; + + __block NSNumber * fabricIndex; + XCTestExpectation * readFabricIndexExpectation1 = + [self expectationWithDescription:@"Fabric index read by initial commissioner"]; + [opCreds1 readAttributeCurrentFabricIndexWithCompletion:^(NSNumber * _Nullable value, NSError * _Nullable readError) { + XCTAssertNil(readError); + XCTAssertNotNil(value); + fabricIndex = value; + [readFabricIndexExpectation1 fulfill]; + }]; + + [self waitForExpectations:@[ readFabricIndexExpectation1 ] timeout:kTimeoutInSeconds]; + + XCTestExpectation * readFabricIndexExpectation2 = [self expectationWithDescription:@"Fabric index read by new node"]; + [opCreds2 readAttributeCurrentFabricIndexWithCompletion:^(NSNumber * _Nullable value, NSError * _Nullable readError) { + XCTAssertNil(readError); + XCTAssertNotNil(value); + XCTAssertEqualObjects(value, fabricIndex); + [readFabricIndexExpectation2 fulfill]; + }]; + + [self waitForExpectations:@[ readFabricIndexExpectation2 ] timeout:kTimeoutInSeconds]; + + // Reset our commissionee. + ResetCommissionee(device1, queue, self, kTimeoutInSeconds); + + [controller1 shutdown]; + XCTAssertFalse([controller1 isRunning]); + [controller2 shutdown]; + XCTAssertFalse([controller2 isRunning]); + [controller3 shutdown]; + XCTAssertFalse([controller3 isRunning]); +} + +@end diff --git a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj index 5481acdf5ea66a..1520c2a9d12082 100644 --- a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj +++ b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj @@ -153,7 +153,13 @@ 51431AF927D2973E008A7943 /* MTRIMDispatch.mm in Sources */ = {isa = PBXBuildFile; fileRef = 51431AF827D2973E008A7943 /* MTRIMDispatch.mm */; }; 51431AFB27D29CA4008A7943 /* ota-provider.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 51431AFA27D29CA4008A7943 /* ota-provider.cpp */; }; 5143851E2A65885500EDC8E6 /* MTRSwiftPairingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5143851D2A65885500EDC8E6 /* MTRSwiftPairingTests.swift */; }; + 514654492A72F9DF00904E61 /* MTRDemuxingStorage.mm in Sources */ = {isa = PBXBuildFile; fileRef = 514654482A72F9DF00904E61 /* MTRDemuxingStorage.mm */; }; + 5146544B2A72F9F500904E61 /* MTRDemuxingStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = 5146544A2A72F9F500904E61 /* MTRDemuxingStorage.h */; }; 51565CAE2A79D42100469F18 /* MTRConversion.mm in Sources */ = {isa = PBXBuildFile; fileRef = 51565CAD2A79D42100469F18 /* MTRConversion.mm */; }; + 51565CB12A7AD77600469F18 /* MTRDeviceControllerDataStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 51565CAF2A7AD77600469F18 /* MTRDeviceControllerDataStore.h */; }; + 51565CB22A7AD77600469F18 /* MTRDeviceControllerDataStore.mm in Sources */ = {isa = PBXBuildFile; fileRef = 51565CB02A7AD77600469F18 /* MTRDeviceControllerDataStore.mm */; }; + 51565CB42A7AD78D00469F18 /* MTRDeviceControllerStorageDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 51565CB32A7AD78D00469F18 /* MTRDeviceControllerStorageDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 51565CB62A7B0D6600469F18 /* MTRDeviceControllerStartupParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 51565CB52A7B0D6600469F18 /* MTRDeviceControllerStartupParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; 515C1C6F284F9FFB00A48F0C /* MTRFramework.mm in Sources */ = {isa = PBXBuildFile; fileRef = 515C1C6D284F9FFB00A48F0C /* MTRFramework.mm */; }; 515C1C70284F9FFB00A48F0C /* MTRFramework.h in Headers */ = {isa = PBXBuildFile; fileRef = 515C1C6E284F9FFB00A48F0C /* MTRFramework.h */; }; 51669AF02913204400F4AA36 /* MTRBackwardsCompatTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 51669AEF2913204400F4AA36 /* MTRBackwardsCompatTests.m */; }; @@ -182,6 +188,9 @@ 51E51FBF282AD37A00FC978D /* MTRDeviceControllerStartupParams.h in Headers */ = {isa = PBXBuildFile; fileRef = 51E51FBC282AD37A00FC978D /* MTRDeviceControllerStartupParams.h */; settings = {ATTRIBUTES = (Public, ); }; }; 51E51FC0282AD37A00FC978D /* MTRDeviceControllerStartupParams_Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 51E51FBD282AD37A00FC978D /* MTRDeviceControllerStartupParams_Internal.h */; }; 51E51FC1282AD37A00FC978D /* MTRDeviceControllerStartupParams.mm in Sources */ = {isa = PBXBuildFile; fileRef = 51E51FBE282AD37A00FC978D /* MTRDeviceControllerStartupParams.mm */; }; + 51E95DF82A78110900A434F0 /* MTRPerControllerStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 51E95DF72A78110900A434F0 /* MTRPerControllerStorageTests.m */; }; + 51E95DFB2A78443C00A434F0 /* MTRSessionResumptionStorageBridge.h in Headers */ = {isa = PBXBuildFile; fileRef = 51E95DF92A78443C00A434F0 /* MTRSessionResumptionStorageBridge.h */; }; + 51E95DFC2A78443C00A434F0 /* MTRSessionResumptionStorageBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = 51E95DFA2A78443C00A434F0 /* MTRSessionResumptionStorageBridge.mm */; }; 51EF279F2A2A3EB100E33F75 /* MTRBackwardsCompatShims.h in Headers */ = {isa = PBXBuildFile; fileRef = 51EF279E2A2A3EB100E33F75 /* MTRBackwardsCompatShims.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5A60370827EA1FF60020DB79 /* MTRClusterStateCacheContainer+XPC.h in Headers */ = {isa = PBXBuildFile; fileRef = 5A60370727EA1FF60020DB79 /* MTRClusterStateCacheContainer+XPC.h */; }; 5A6FEC9027B563D900F25F42 /* MTRDeviceControllerOverXPC.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5A6FEC8F27B563D900F25F42 /* MTRDeviceControllerOverXPC.mm */; }; @@ -457,7 +466,13 @@ 51431AFA27D29CA4008A7943 /* ota-provider.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = "ota-provider.cpp"; path = "clusters/ota-provider/ota-provider.cpp"; sourceTree = ""; }; 5143851C2A65885400EDC8E6 /* MatterTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MatterTests-Bridging-Header.h"; sourceTree = ""; }; 5143851D2A65885500EDC8E6 /* MTRSwiftPairingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MTRSwiftPairingTests.swift; sourceTree = ""; }; + 514654482A72F9DF00904E61 /* MTRDemuxingStorage.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRDemuxingStorage.mm; sourceTree = ""; }; + 5146544A2A72F9F500904E61 /* MTRDemuxingStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRDemuxingStorage.h; sourceTree = ""; }; 51565CAD2A79D42100469F18 /* MTRConversion.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRConversion.mm; sourceTree = ""; }; + 51565CAF2A7AD77600469F18 /* MTRDeviceControllerDataStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRDeviceControllerDataStore.h; sourceTree = ""; }; + 51565CB02A7AD77600469F18 /* MTRDeviceControllerDataStore.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRDeviceControllerDataStore.mm; sourceTree = ""; }; + 51565CB32A7AD78D00469F18 /* MTRDeviceControllerStorageDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRDeviceControllerStorageDelegate.h; sourceTree = ""; }; + 51565CB52A7B0D6600469F18 /* MTRDeviceControllerStartupParameters.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRDeviceControllerStartupParameters.h; sourceTree = ""; }; 515C1C6D284F9FFB00A48F0C /* MTRFramework.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRFramework.mm; sourceTree = ""; }; 515C1C6E284F9FFB00A48F0C /* MTRFramework.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRFramework.h; sourceTree = ""; }; 51669AEF2913204400F4AA36 /* MTRBackwardsCompatTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRBackwardsCompatTests.m; sourceTree = ""; }; @@ -488,6 +503,9 @@ 51E51FBC282AD37A00FC978D /* MTRDeviceControllerStartupParams.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRDeviceControllerStartupParams.h; sourceTree = ""; }; 51E51FBD282AD37A00FC978D /* MTRDeviceControllerStartupParams_Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRDeviceControllerStartupParams_Internal.h; sourceTree = ""; }; 51E51FBE282AD37A00FC978D /* MTRDeviceControllerStartupParams.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRDeviceControllerStartupParams.mm; sourceTree = ""; }; + 51E95DF72A78110900A434F0 /* MTRPerControllerStorageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRPerControllerStorageTests.m; sourceTree = ""; }; + 51E95DF92A78443C00A434F0 /* MTRSessionResumptionStorageBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRSessionResumptionStorageBridge.h; sourceTree = ""; }; + 51E95DFA2A78443C00A434F0 /* MTRSessionResumptionStorageBridge.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRSessionResumptionStorageBridge.mm; sourceTree = ""; }; 51EF279E2A2A3EB100E33F75 /* MTRBackwardsCompatShims.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRBackwardsCompatShims.h; sourceTree = ""; }; 5A60370727EA1FF60020DB79 /* MTRClusterStateCacheContainer+XPC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MTRClusterStateCacheContainer+XPC.h"; sourceTree = ""; }; 5A6FEC8B27B5609C00F25F42 /* MTRDeviceOverXPC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRDeviceOverXPC.h; sourceTree = ""; }; @@ -1045,6 +1063,8 @@ 5A7947E227C0101200434CF2 /* MTRDeviceController+XPC.h */, 5A7947E327C0129500434CF2 /* MTRDeviceController+XPC.mm */, 2CB7163E252F731E0026E2BB /* MTRDeviceControllerDelegate.h */, + 51565CAF2A7AD77600469F18 /* MTRDeviceControllerDataStore.h */, + 51565CB02A7AD77600469F18 /* MTRDeviceControllerDataStore.mm */, 2CB71638252E8A7B0026E2BB /* MTRDeviceControllerDelegateBridge.h */, 2CB71639252E8A7B0026E2BB /* MTRDeviceControllerDelegateBridge.mm */, 5136661228067D550025EDAE /* MTRDeviceControllerFactory.h */, @@ -1056,6 +1076,8 @@ 51E51FBC282AD37A00FC978D /* MTRDeviceControllerStartupParams.h */, 51E51FBD282AD37A00FC978D /* MTRDeviceControllerStartupParams_Internal.h */, 51E51FBE282AD37A00FC978D /* MTRDeviceControllerStartupParams.mm */, + 51565CB52A7B0D6600469F18 /* MTRDeviceControllerStartupParameters.h */, + 51565CB32A7AD78D00469F18 /* MTRDeviceControllerStorageDelegate.h */, 5A6FEC9427B5976200F25F42 /* MTRDeviceControllerXPCConnection.h */, 5A6FEC9527B5983000F25F42 /* MTRDeviceControllerXPCConnection.mm */, 5A6FEC8B27B5609C00F25F42 /* MTRDeviceOverXPC.h */, @@ -1088,6 +1110,10 @@ 998F287026D56940001846C6 /* MTRP256KeypairBridge.mm */, 2C8C8FBD253E0C2100797F05 /* MTRPersistentStorageDelegateBridge.h */, 2C8C8FBF253E0C2100797F05 /* MTRPersistentStorageDelegateBridge.mm */, + 514654482A72F9DF00904E61 /* MTRDemuxingStorage.mm */, + 5146544A2A72F9F500904E61 /* MTRDemuxingStorage.h */, + 51E95DF92A78443C00A434F0 /* MTRSessionResumptionStorageBridge.h */, + 51E95DFA2A78443C00A434F0 /* MTRSessionResumptionStorageBridge.mm */, B2E0D7AC245B0B5C003C5B48 /* MTRQRCodeSetupPayloadParser.h */, B2E0D7AE245B0B5C003C5B48 /* MTRQRCodeSetupPayloadParser.mm */, B2E0D7AF245B0B5C003C5B48 /* MTRSetupPayload.h */, @@ -1132,6 +1158,7 @@ 51339B1E2A0DA64D00C798C1 /* MTRCertificateValidityTests.m */, 519498312A25581C00B3BABE /* MTRSetupPayloadSerializerTests.m */, 5143851D2A65885500EDC8E6 /* MTRSwiftPairingTests.swift */, + 51E95DF72A78110900A434F0 /* MTRPerControllerStorageTests.m */, B202529D2459E34F00F97062 /* Info.plist */, 5143851C2A65885400EDC8E6 /* MatterTests-Bridging-Header.h */, ); @@ -1198,6 +1225,8 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 51565CB62A7B0D6600469F18 /* MTRDeviceControllerStartupParameters.h in Headers */, + 51565CB42A7AD78D00469F18 /* MTRDeviceControllerStorageDelegate.h in Headers */, 510A07492A685D3900A9241C /* Matter.apinotes in Headers */, 51EF279F2A2A3EB100E33F75 /* MTRBackwardsCompatShims.h in Headers */, 5173A47729C0E2ED00F67F48 /* MTRFabricInfo.h in Headers */, @@ -1273,12 +1302,15 @@ 2C222ADF255C811800E446B9 /* MTRBaseDevice_Internal.h in Headers */, 511913FC28C100EF009235E9 /* MTRBaseSubscriptionCallback.h in Headers */, 51E0310027EA20D20083DC9C /* MTRControllerAccessControl.h in Headers */, + 51565CB12A7AD77600469F18 /* MTRDeviceControllerDataStore.h in Headers */, 3D843713294977000070D20A /* NSDataSpanConversion.h in Headers */, 991DC08B247704DC00C13860 /* MTRLogging_Internal.h in Headers */, 1E4D655029C208DD00BC3478 /* MTRCommissionableBrowserDelegate.h in Headers */, 7596A84828762783004DAE0E /* MTRAsyncCallbackWorkQueue.h in Headers */, 5A7947E527C0129F00434CF2 /* MTRDeviceController+XPC.h in Headers */, + 51E95DFB2A78443C00A434F0 /* MTRSessionResumptionStorageBridge.h in Headers */, B2E0D7B4245B0B5C003C5B48 /* MTRError_Internal.h in Headers */, + 5146544B2A72F9F500904E61 /* MTRDemuxingStorage.h in Headers */, 1EDCE545289049A100E41EC9 /* MTROTAHeader.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1485,6 +1517,7 @@ 75B765C32A1D82D30014719B /* MTRAttributeSpecifiedCheck.mm in Sources */, AF5F90FF2878D351005503FA /* MTROTAProviderDelegateBridge.mm in Sources */, 3D84374B29498BAE0070D20A /* privilege-storage.cpp in Sources */, + 51E95DFC2A78443C00A434F0 /* MTRSessionResumptionStorageBridge.mm in Sources */, 7534F12828BFF20300390851 /* MTRDeviceAttestationDelegate.mm in Sources */, 2C5EEEF7268A85C400CAE3D3 /* MTRDeviceConnectionBridge.mm in Sources */, 51B22C262740CB32008D5055 /* MTRStructsObjc.mm in Sources */, @@ -1496,6 +1529,7 @@ 514304202914CED9004DC7FE /* generic-callback-stubs.cpp in Sources */, 1EDCE546289049A100E41EC9 /* MTROTAHeader.mm in Sources */, 1EC4CE5D25CC26E900D7304F /* MTRBaseClusters.mm in Sources */, + 51565CB22A7AD77600469F18 /* MTRDeviceControllerDataStore.mm in Sources */, 51E0310127EA20D20083DC9C /* MTRControllerAccessControl.mm in Sources */, 1ED276E226C5812A00547A89 /* MTRCluster.mm in Sources */, B2E0D7B3245B0B5C003C5B48 /* MTRError.mm in Sources */, @@ -1523,6 +1557,7 @@ 7596A85528788557004DAE0E /* MTRClusters.mm in Sources */, 88EBF8CF27FABDD500686BC1 /* MTRDeviceAttestationDelegateBridge.mm in Sources */, 5A6FEC9827B5C6AF00F25F42 /* MTRDeviceOverXPC.mm in Sources */, + 514654492A72F9DF00904E61 /* MTRDemuxingStorage.mm in Sources */, 51431AF927D2973E008A7943 /* MTRIMDispatch.mm in Sources */, 1E4D655229C30A8700BC3478 /* MTRCommissionableBrowser.mm in Sources */, 51431AFB27D29CA4008A7943 /* ota-provider.cpp in Sources */, @@ -1561,6 +1596,7 @@ 51E24E73274E0DAC007CCF6E /* MTRErrorTestUtils.mm in Sources */, 519498322A25581C00B3BABE /* MTRSetupPayloadSerializerTests.m in Sources */, 51A2F1322A00402A00F03298 /* MTRDataValueParserTests.m in Sources */, + 51E95DF82A78110900A434F0 /* MTRPerControllerStorageTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };