CKKSKey.m   [plain text]

/*
* Copyright (c) 2017 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this
* file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_LICENSE_HEADER_END@
*/

#if OCTAGON

#import "CKKSViewManager.h"
#import "CKKSKeychainView.h"
#import "CKKSCurrentKeyPointer.h"
#import "CKKSKey.h"
#import "keychain/categories/NSError+UsefulConstructors.h"
#include <securityd/SecItemSchema.h>
#include <Security/SecItem.h>
#include <Security/SecItemPriv.h>

#include <CloudKit/CloudKit.h>
#include <CloudKit/CloudKit_Private.h>

@interface CKKSKey ()
@property CKKSAESSIVKey* aessivkey;
@end

@implementation CKKSKey

- (instancetype)init {
self = [super init];
return self;
}

- (instancetype) initSelfWrappedWithAESKey: (CKKSAESSIVKey*) aeskey
uuid: (NSString*) uuid
keyclass: (CKKSKeyClass*)keyclass
state: (CKKSProcessedState*) state
zoneID: (CKRecordZoneID*) zoneID
encodedCKRecord: (NSData*) encodedrecord
currentkey: (NSInteger) currentkey
{
if(self = [super initWithUUID: uuid
parentKeyUUID: uuid
zoneID: zoneID
encodedCKRecord: encodedrecord
encItem: nil
wrappedkey: nil
generationCount: 0
encver: currentCKKSItemEncryptionVersion]) {
_keyclass = keyclass;
_currentkey = !!currentkey;
_aessivkey = aeskey;
_state = state;

self.ckRecordType = SecCKRecordIntermediateKeyType;

// Wrap the key with the key. Not particularly useful, but there you go.
NSError* error = nil;
[self wrapUnder: self error:&error];
if(error != nil) {
secerror("CKKSKey: Couldn't self-wrap key: %@", error);
return nil;
}
}
return self;
}

- (instancetype) initWrappedBy: (CKKSKey*) wrappingKey
AESKey: (CKKSAESSIVKey*) aeskey
uuid: (NSString*) uuid
keyclass: (CKKSKeyClass*)keyclass
state: (CKKSProcessedState*) state
zoneID: (CKRecordZoneID*) zoneID
encodedCKRecord: (NSData*) encodedrecord
currentkey: (NSInteger) currentkey
{
if(self = [super initWithUUID: uuid
parentKeyUUID: wrappingKey.uuid
zoneID: zoneID
encodedCKRecord: encodedrecord
encItem:nil
wrappedkey:nil
generationCount:0
encver:
currentCKKSItemEncryptionVersion]) {
_keyclass = keyclass;
_currentkey = !!currentkey;
_aessivkey = aeskey;
_state = state;

self.ckRecordType = SecCKRecordIntermediateKeyType;

NSError* error = nil;
[self wrapUnder: wrappingKey error:&error];
if(error != nil) {
secerror("CKKSKey: Couldn't wrap key with key: %@", error);
return nil;
}
}
return self;
}

- (instancetype) initWithWrappedAESKey: (CKKSWrappedAESSIVKey*) wrappedaeskey
uuid: (NSString*) uuid
parentKeyUUID: (NSString*) parentKeyUUID
keyclass: (CKKSKeyClass*)keyclass
state: (CKKSProcessedState*) state
zoneID: (CKRecordZoneID*) zoneID
encodedCKRecord: (NSData*) encodedrecord
currentkey: (NSInteger) currentkey
{
if(self = [super initWithUUID:uuid
parentKeyUUID:parentKeyUUID
zoneID:zoneID
encodedCKRecord:encodedrecord
encItem:nil
wrappedkey:wrappedaeskey
generationCount:0
encver:currentCKKSItemEncryptionVersion]) {
_keyclass = keyclass;
_currentkey = !!currentkey;
_aessivkey = nil;
_state = state;

self.ckRecordType = SecCKRecordIntermediateKeyType;
}
return self;
}

- (void)dealloc {
[self zeroKeys];
}

- (void)zeroKeys {
[self.aessivkey zeroKey];
}

- (bool)wrapsSelf {
return [self.uuid isEqual: self.parentKeyUUID];
}

- (bool)wrapUnder: (CKKSKey*) wrappingKey error: (NSError * __autoreleasing *) error {
self.wrappedkey = [wrappingKey wrapAESKey: self.aessivkey error:error];
if(self.wrappedkey == nil) {
secerror("CKKSKey: couldn't wrap key: %@", error ? *error : @"unknown error");
} else {
self.parentKeyUUID = wrappingKey.uuid;
}
return (self.wrappedkey != nil);
}

- (bool)unwrapSelfWithAESKey: (CKKSAESSIVKey*) unwrappingKey error: (NSError * __autoreleasing *) error {
_aessivkey = [unwrappingKey unwrapAESKey:self.wrappedkey error:error];
return (_aessivkey != nil);
}

+ (instancetype) loadKeyWithUUID: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
CKKSKey* key = [CKKSKey fromDatabase: uuid zoneID:zoneID error:error];

// failed unwrapping means we can't return a key.
if(![key ensureKeyLoaded:error]) {
return nil;
}
return key;
}

+ (CKKSKey*) randomKeyWrappedByParent: (CKKSKey*) parentKey error: (NSError * __autoreleasing *) error {
return [self randomKeyWrappedByParent: parentKey keyclass:parentKey.keyclass error:error];
}

+ (CKKSKey*) randomKeyWrappedByParent: (CKKSKey*) parentKey keyclass:(CKKSKeyClass*)keyclass error: (NSError * __autoreleasing *) error {
CKKSAESSIVKey* aessivkey = [CKKSAESSIVKey randomKey];

CKKSKey* key = [[CKKSKey alloc] initWrappedBy: parentKey
AESKey: aessivkey
uuid:[[NSUUID UUID] UUIDString]
keyclass:keyclass
state:SecCKKSProcessedStateLocal
zoneID: parentKey.zoneID
encodedCKRecord: nil
currentkey: false];
return key;
}

+ (instancetype)randomKeyWrappedBySelf: (CKRecordZoneID*) zoneID error: (NSError * __autoreleasing *) error {
CKKSAESSIVKey* aessivkey = [CKKSAESSIVKey randomKey];
NSString* uuid = [[NSUUID UUID] UUIDString];

CKKSKey* key = [[CKKSKey alloc] initSelfWrappedWithAESKey: aessivkey
uuid:uuid
keyclass:SecCKKSKeyClassTLK
state:SecCKKSProcessedStateLocal
zoneID: zoneID
encodedCKRecord: nil
currentkey: false];
return key;

}

- (CKKSKey*)topKeyInAnyState: (NSError * __autoreleasing *) error {
// Recursively find the top-level key in the hierarchy, preferring 'remote' keys.
if([self wrapsSelf]) {
return self;
}

CKKSKey* remoteParent = [CKKSKey tryFromDatabaseWhere: @{@"UUID": self.parentKeyUUID, @"state": SecCKKSProcessedStateRemote} error: error];
if(remoteParent) {
return [remoteParent topKeyInAnyState: error];
}

// No remote parent. Fall back to anything.
CKKSKey* parent = [CKKSKey fromDatabaseWhere: @{@"UUID": self.parentKeyUUID} error: error];
if(parent) {
return [parent topKeyInAnyState: error];
}

// No good. Error is already filled.
return nil;
}

- (CKKSAESSIVKey*)ensureKeyLoaded: (NSError * __autoreleasing *) error {
if(self.aessivkey) {
return self.aessivkey;
}

// Attempt to load this key from the keychain
NSError* keychainError = nil;
if([self loadKeyMaterialFromKeychain:&keychainError]) {
return self.aessivkey;
}

// Uhh, okay, if that didn't work, try to unwrap via the key hierarchy
NSError* keyHierarchyError = nil;
if([self unwrapViaKeyHierarchy:&keyHierarchyError]) {
// Attempt to save this new key, but don't error if it fails
NSError* resaveError = nil;
if(![self saveKeyMaterialToKeychain:&resaveError] || resaveError) {
secerror("ckkskey: Resaving missing key failed, continuing: %@", resaveError);
}

return self.aessivkey;
}

// Pick an error to report
if(error) {
*error = keyHierarchyError ? keyHierarchyError : keychainError;
}

return nil;
}

- (CKKSAESSIVKey*)unwrapViaKeyHierarchy: (NSError * __autoreleasing *) error {
if(self.aessivkey) {
return self.aessivkey;
}

NSError* localerror = nil;

// Attempt to load this key from the keychain
if([self loadKeyMaterialFromKeychain:&localerror]) {
// Rad. Success!
return self.aessivkey;
}

// First, check if we're a TLK.
if([self.keyclass isEqual: SecCKKSKeyClassTLK]) {
// Okay, not loading the key from the keychain above is an issue. If we have a parent key, then fall through to the recursion below.
if(!self.parentKeyUUID || [self.parentKeyUUID isEqualToString: self.uuid]) {
if(error) {
*error = localerror;
}
return nil;
}
}

// Recursively unwrap our parent.
CKKSKey* parent = [CKKSKey fromDatabaseAnyState:self.parentKeyUUID zoneID:self.zoneID error:error];

// TODO: do we need loop detection here?
if(![parent unwrapViaKeyHierarchy: error]) {
return nil;
}

_aessivkey = [parent unwrapAESKey:self.wrappedkey error:error];
return self.aessivkey;
}

- (bool)trySelfWrappedKeyCandidate:(CKKSAESSIVKey*)candidate error:(NSError * __autoreleasing *) error {
if(![self wrapsSelf]) {
if(error) {
*error = [NSError errorWithDomain:CKKSErrorDomain code:CKKSKeyNotSelfWrapped userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"%@ is not self-wrapped", self]}];
}
return false;
}

CKKSAESSIVKey* unwrapped = [candidate unwrapAESKey:self.wrappedkey error:error];
if(unwrapped && [unwrapped isEqual: candidate]) {
_aessivkey = unwrapped;
return true;
}

return false;
}

- (CKKSWrappedAESSIVKey*)wrapAESKey: (CKKSAESSIVKey*) keyToWrap error: (NSError * __autoreleasing *) error {
CKKSAESSIVKey* key = [self ensureKeyLoaded: error];
CKKSWrappedAESSIVKey* wrappedkey = [key wrapAESKey: keyToWrap error:error];
return wrappedkey;
}

- (CKKSAESSIVKey*)unwrapAESKey: (CKKSWrappedAESSIVKey*) keyToUnwrap error: (NSError * __autoreleasing *) error {
CKKSAESSIVKey* key = [self ensureKeyLoaded: error];
CKKSAESSIVKey* unwrappedkey = [key unwrapAESKey: keyToUnwrap error:error];
return unwrappedkey;
}

- (NSData*)encryptData: (NSData*) plaintext authenticatedData: (NSDictionary<NSString*, NSData*>*) ad error: (NSError * __autoreleasing *) error {
CKKSAESSIVKey* key = [self ensureKeyLoaded: error];
NSData* data = [key encryptData: plaintext authenticatedData:ad error:error];
return data;
}

- (NSData*)decryptData: (NSData*) ciphertext authenticatedData: (NSDictionary<NSString*, NSData*>*) ad error: (NSError * __autoreleasing *) error {
CKKSAESSIVKey* key = [self ensureKeyLoaded: error];
NSData* data = [key decryptData: ciphertext authenticatedData:ad error:error];
return data;
}

/* Functions to load and save keys from the keychain (where we get to store actual key material!) */
- (bool)saveKeyMaterialToKeychain: (NSError * __autoreleasing *) error {
return [self saveKeyMaterialToKeychain: true error: error];
}

- (bool)saveKeyMaterialToKeychain: (bool)stashTLK error:(NSError * __autoreleasing *) error {

// Note that we only store the key class, view, UUID, parentKeyUUID, and key material in the keychain
// Any other metadata must be stored elsewhere and filled in at load time.

if(![self ensureKeyLoaded:error]) {
// No key material, nothing to save to keychain.
return false;
}

// iOS keychains can't store symmetric keys, so we're reduced to storing this key as a password
NSData* keydata = [[[NSData alloc] initWithBytes:self.aessivkey->key length:self.aessivkey->size] base64EncodedDataWithOptions:0];
NSMutableDictionary* query = [@{
(id)kSecClass : (id)kSecClassInternetPassword,
(id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
(id)kSecAttrNoLegacy : @YES,
(id)kSecAttrAccessGroup: @"com.apple.security.ckks",
(id)kSecAttrDescription: self.keyclass,
(id)kSecAttrServer: self.zoneID.zoneName,
(id)kSecAttrAccount: self.uuid,
(id)kSecAttrPath: self.parentKeyUUID,
(id)kSecAttrIsInvisible: @YES,
(id)kSecValueData : keydata,
} mutableCopy];

// Only TLKs are synchronizable. Other keyclasses must synchronize via key hierarchy.
if([self.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
// Use PCS-MasterKey view so they'll be initial-synced under SOS.
query[(id)kSecAttrSyncViewHint] = (id)kSecAttrViewHintPCSMasterKey;
query[(id)kSecAttrSynchronizable] = (id)kCFBooleanTrue;
}

// Class C keys are accessible after first unlock; TLKs and Class A keys are accessible only when unlocked
if([self.keyclass isEqualToString: SecCKKSKeyClassC]) {
query[(id)kSecAttrAccessible] = (id)kSecAttrAccessibleAfterFirstUnlock;
} else {
query[(id)kSecAttrAccessible] = (id)kSecAttrAccessibleWhenUnlocked;
}

NSError* localError = nil;
[CKKSKey setKeyMaterialInKeychain:query error:&localError];

if(localError && error) {
*error = [NSError errorWithDomain:@"securityd"
code:localError.code
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Couldn't save %@ to keychain: %d", self, (int)localError.code],
NSUnderlyingErrorKey: localError,
}];
}

// TLKs are synchronizable. Stash them nonsyncably nearby.
// Don't report errors here.
if(stashTLK && [self.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
query = [@{
(id)kSecClass : (id)kSecClassInternetPassword,
(id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
(id)kSecAttrNoLegacy : @YES,
(id)kSecAttrAccessGroup: @"com.apple.security.ckks",
(id)kSecAttrDescription: [self.keyclass stringByAppendingString: @"-nonsync"],
(id)kSecAttrServer: self.zoneID.zoneName,
(id)kSecAttrAccount: self.uuid,
(id)kSecAttrPath: self.parentKeyUUID,
(id)kSecAttrIsInvisible: @YES,
(id)kSecValueData : keydata,
} mutableCopy];
query[(id)kSecAttrSynchronizable] = (id)kCFBooleanFalse;

NSError* stashError = nil;
[CKKSKey setKeyMaterialInKeychain:query error:&localError];

if(stashError) {
secerror("ckkskey: Couldn't stash %@ to keychain: %@", self, stashError);
}
}

return localError == nil;
}

+ (NSDictionary*)setKeyMaterialInKeychain:(NSDictionary*)query error:(NSError* __autoreleasing *)error {
CFTypeRef result = NULL;
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, &result);

NSError* localerror = nil;

// Did SecItemAdd fall over due to an existing item?
if(status == errSecDuplicateItem) {
// Add every primary key attribute to this find dictionary
NSMutableDictionary* findQuery = [[NSMutableDictionary alloc] init];
findQuery[(id)kSecClass] = query[(id)kSecClass];
findQuery[(id)kSecAttrSynchronizable] = query[(id)kSecAttrSynchronizable];
findQuery[(id)kSecAttrSyncViewHint] = query[(id)kSecAttrSyncViewHint];
findQuery[(id)kSecAttrAccessGroup] = query[(id)kSecAttrAccessGroup];
findQuery[(id)kSecAttrAccount] = query[(id)kSecAttrAccount];
findQuery[(id)kSecAttrServer] = query[(id)kSecAttrServer];
findQuery[(id)kSecAttrPath] = query[(id)kSecAttrPath];

NSMutableDictionary* updateQuery = [query mutableCopy];
updateQuery[(id)kSecClass] = nil;

status = SecItemUpdate((__bridge CFDictionaryRef)findQuery, (__bridge CFDictionaryRef)updateQuery);

if(status) {
localerror = [NSError errorWithDomain:@"securityd"
code:status
description:[NSString stringWithFormat:@"SecItemUpdate: %d", (int)status]];
}
} else {
localerror = [NSError errorWithDomain:@"securityd"
code:status
description: [NSString stringWithFormat:@"SecItemAdd: %d", (int)status]];
}

if(status) {
CFReleaseNull(result);

if(error) {
*error = localerror;
}
return false;
}

NSDictionary* resultDict = CFBridgingRelease(result);
return resultDict;
}

+ (NSDictionary*)queryKeyMaterialInKeychain:(NSDictionary*)query error:(NSError* __autoreleasing *)error {
CFTypeRef result = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);

if(status) {
CFReleaseNull(result);

if(error) {
*error = [NSError errorWithDomain:@"securityd"
code:status
userInfo:@{NSLocalizedDescriptionKey:
[NSString stringWithFormat:@"SecItemCopyMatching: %d", (int)status]}];
}
return false;
}

NSDictionary* resultDict = CFBridgingRelease(result);
return resultDict;
}

+ (NSDictionary*)fetchKeyMaterialItemFromKeychain:(CKKSKey*)key resave:(bool*)resavePtr error:(NSError* __autoreleasing *)error {
NSMutableDictionary* query = [@{
(id)kSecClass : (id)kSecClassInternetPassword,
(id)kSecAttrNoLegacy : @YES,
(id)kSecAttrAccessGroup : @"com.apple.security.ckks",
(id)kSecAttrDescription: key.keyclass,
(id)kSecAttrAccount: key.uuid,
(id)kSecAttrServer: key.zoneID.zoneName,
(id)kSecAttrPath: key.parentKeyUUID,
(id)kSecReturnAttributes: @YES,
(id)kSecReturnData: @YES,
} mutableCopy];

// Synchronizable items are only found if you request synchronizable items. Only TLKs are synchronizable.
if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
query[(id)kSecAttrSynchronizable] = (id)kCFBooleanTrue;
}

NSError* localError = nil;
NSDictionary* result = [self queryKeyMaterialInKeychain:query error:&localError];
NSError* originalError = localError;

// If we found the item or errored in some interesting way, return.
if(result) {
return result;
}
if(localError && localError.code != errSecItemNotFound) {
if(error) {
*error = [NSError errorWithDomain:@"securityd"
code:localError.code
userInfo:@{NSLocalizedDescriptionKey:
[NSString stringWithFormat:@"Couldn't load %@ from keychain: %d", key, (int)localError.code],
NSUnderlyingErrorKey: localError,
}];
}
return result;
}
localError = nil;

if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
//didn't find a regular tlk? how about a piggy?
query = [@{
(id)kSecClass : (id)kSecClassInternetPassword,
(id)kSecAttrNoLegacy : @YES,
(id)kSecAttrAccessGroup : @"com.apple.security.ckks",
(id)kSecAttrDescription: [key.keyclass stringByAppendingString: @"-piggy"],
(id)kSecAttrSynchronizable : (id)kSecAttrSynchronizableAny,
(id)kSecAttrAccount: [NSString stringWithFormat: @"%@-piggy", key.uuid],
(id)kSecAttrServer: key.zoneID.zoneName,
(id)kSecReturnAttributes: @YES,
(id)kSecReturnData: @YES,
(id)kSecMatchLimit: (id)kSecMatchLimitOne,
} mutableCopy];

result = [self queryKeyMaterialInKeychain:query error:&localError];
if(localError == nil) {
secnotice("ckkskey", "loaded a piggy TLK (%@)", key.uuid);

if(resavePtr) {
*resavePtr = true;
}

return result;
}

if(localError && localError.code != errSecItemNotFound) {
if(error) {
*error = [NSError errorWithDomain:@"securityd"
code:localError.code
userInfo:@{NSLocalizedDescriptionKey:
[NSString stringWithFormat:@"Couldn't load %@ from keychain: %d", key, (int)localError.code],
NSUnderlyingErrorKey: localError,
}];
}
return nil;
}
}

localError = nil;

// Try to load a stashed TLK
if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
localError = nil;

// Try to look for the non-syncable stashed tlk and resurrect it.
query = [@{
(id)kSecClass : (id)kSecClassInternetPassword,
(id)kSecAttrNoLegacy : @YES,
(id)kSecAttrAccessGroup : @"com.apple.security.ckks",
(id)kSecAttrDescription: [key.keyclass stringByAppendingString: @"-nonsync"],
(id)kSecAttrServer: key.zoneID.zoneName,
(id)kSecAttrAccount: key.uuid,
(id)kSecReturnAttributes: @YES,
(id)kSecReturnData: @YES,
(id)kSecAttrSynchronizable: @NO,
} mutableCopy];

result = [self queryKeyMaterialInKeychain:query error:&localError];
if(localError == nil) {
secnotice("ckkskey", "loaded a stashed TLK (%@)", key.uuid);

if(resavePtr) {
*resavePtr = true;
}

return result;
}

if(localError && localError.code != errSecItemNotFound) {
if(error) {
*error = [NSError errorWithDomain:@"securityd"
code:localError.code
userInfo:@{NSLocalizedDescriptionKey:
[NSString stringWithFormat:@"Couldn't load %@ from keychain: %d", key, (int)localError.code],
NSUnderlyingErrorKey: localError,
}];
}
return nil;
}
}

// We didn't early-return. Use whatever error the original fetch produced.
if(error) {
*error = [NSError errorWithDomain:@"securityd"
code:originalError ? originalError.code : errSecParam
description:[NSString stringWithFormat:@"Couldn't load %@ from keychain: %d", key, (int)originalError.code]
underlying:originalError];
}

return result;
}

- (bool)loadKeyMaterialFromKeychain: (NSError * __autoreleasing *) error {
bool resave = false;
NSDictionary* result = [CKKSKey fetchKeyMaterialItemFromKeychain:self resave:&resave error:error];
if(!result) {
return false;
}

NSData* b64keymaterial = result[(id)kSecValueData];
NSData* keymaterial = [[NSData alloc] initWithBase64EncodedData:b64keymaterial options:0];
if(!keymaterial) {
secnotice("ckkskey", "Unable to unbase64 key: %@", self);
if(error) {
*error = [NSError errorWithDomain:CKKSErrorDomain
code:CKKSKeyUnknownFormat
description:[NSString stringWithFormat:@"unable to unbase64 key: %@", self]];
}
return false;
}

CKKSAESSIVKey* key = [[CKKSAESSIVKey alloc] initWithBytes: (uint8_t*) keymaterial.bytes len:keymaterial.length];
self.aessivkey = key;

if(resave) {
secnotice("ckkskey", "Resaving %@ as per request", self);
NSError* resaveError = nil;
[self saveKeyMaterialToKeychain:&resaveError];
if(resaveError) {
secnotice("ckkskey", "Resaving %@ failed: %@", self, resaveError);
}
}

return !!(self.aessivkey);
}

- (bool)deleteKeyMaterialFromKeychain: (NSError * __autoreleasing *) error {

NSMutableDictionary* query = [@{
(id)kSecClass : (id)kSecClassInternetPassword,
(id)kSecAttrNoLegacy : @YES,
(id)kSecAttrAccessGroup : @"com.apple.security.ckks",
(id)kSecAttrDescription: self.keyclass,
(id)kSecAttrAccount: self.uuid,
(id)kSecAttrServer: self.zoneID.zoneName,
(id)kSecReturnData: @YES,
} mutableCopy];

// Synchronizable items are only found if you request synchronizable items. Only TLKs are synchronizable.
if([self.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
query[(id)kSecAttrSynchronizable] = (id)kCFBooleanTrue;
}

OSStatus status = SecItemDelete((__bridge CFDictionaryRef) query);

if(status) {
if(error) {
*error = [NSError errorWithDomain:@"securityd"
code:status
userInfo:@{NSLocalizedDescriptionKey:
[NSString stringWithFormat:@"Couldn't delete %@ from keychain: %d", self, (int)status]}];
}
return false;
}
return true;
}

+ (instancetype)keyFromKeychain: (NSString*) uuid
parentKeyUUID: (NSString*) parentKeyUUID
keyclass: (CKKSKeyClass*)keyclass
state: (CKKSProcessedState*) state
zoneID: (CKRecordZoneID*) zoneID
encodedCKRecord: (NSData*) encodedrecord
currentkey: (NSInteger) currentkey
error: (NSError * __autoreleasing *) error {
CKKSKey* key = [[CKKSKey alloc] initWithWrappedAESKey:nil
uuid:uuid
parentKeyUUID:parentKeyUUID
keyclass:keyclass
state:state
zoneID:zoneID
encodedCKRecord:encodedrecord
currentkey:currentkey];

if(![key loadKeyMaterialFromKeychain:error]) {
return nil;
}

return key;
}

+ (NSString*)isItemKeyForKeychainView: (SecDbItemRef) item {

NSString* accessgroup = (__bridge NSString*) SecDbItemGetCachedValueWithName(item, kSecAttrAccessGroup);
NSString* description = (__bridge NSString*) SecDbItemGetCachedValueWithName(item, kSecAttrDescription);
NSString* server = (__bridge NSString*) SecDbItemGetCachedValueWithName(item, kSecAttrServer);

if(accessgroup && description && server &&
![accessgroup isEqual:[NSNull null]] &&
![description isEqual:[NSNull null]] &&
![server isEqual:[NSNull null]] &&

[accessgroup isEqualToString:@"com.apple.security.ckks"] &&
([description isEqualToString: SecCKKSKeyClassTLK] ||
[description isEqualToString: [NSString stringWithFormat:@"%@-nonsync", SecCKKSKeyClassTLK]] ||
[description isEqualToString: [NSString stringWithFormat:@"%@-piggy", SecCKKSKeyClassTLK]] ||
[description isEqualToString: SecCKKSKeyClassA] ||
[description isEqualToString: SecCKKSKeyClassC])) {

// Certainly looks like us! Return the view name.
return server;
}

// Never heard of this item.
return nil;
}

/* Database functions only return keys marked 'local', unless otherwise specified. */

+ (instancetype) fromDatabase: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
return [self fromDatabaseWhere: @{@"UUID": uuid, @"state": SecCKKSProcessedStateLocal, @"ckzone":zoneID.zoneName} error: error];
}

+ (instancetype) fromDatabaseAnyState: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
return [self fromDatabaseWhere: @{@"UUID": uuid, @"ckzone":zoneID.zoneName} error: error];
}

+ (instancetype) tryFromDatabase: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
return [self tryFromDatabaseWhere: @{@"UUID": uuid, @"state": SecCKKSProcessedStateLocal, @"ckzone":zoneID.zoneName} error: error];
}

+ (instancetype) tryFromDatabaseAnyState: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
return [self tryFromDatabaseWhere: @{@"UUID": uuid, @"ckzone":zoneID.zoneName} error: error];
}

+ (NSArray<CKKSKey*>*)selfWrappedKeys:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
return [self allWhere: @{@"UUID": [CKKSSQLWhereObject op:@"=" string:@"parentKeyUUID"], @"state": SecCKKSProcessedStateLocal, @"ckzone":zoneID.zoneName} error:error];
}

+ (instancetype) currentKeyForClass: (CKKSKeyClass*) keyclass zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
// Load the CurrentKey record, and find the key for it
CKKSCurrentKeyPointer* ckp = [CKKSCurrentKeyPointer fromDatabase:keyclass zoneID:zoneID error:error];
if(!ckp) {
return nil;
}
return [self fromDatabase:ckp.currentKeyUUID zoneID:zoneID error:error];
}

+ (NSArray<CKKSKey*>*) currentKeysForClass: (CKKSKeyClass*) keyclass state:(NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
return [self allWhere: @{@"keyclass": keyclass, @"currentkey": @"1", @"state": state ? state : SecCKKSProcessedStateLocal, @"ckzone":zoneID.zoneName} error:error];
}

/* Returns all keys for a zone */
+ (NSArray<CKKSKey*>*)allKeys: (CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
return [self allWhere: @{@"ckzone":zoneID.zoneName} error:error];
}

/* Returns all keys marked 'remote', i.e., downloaded from CloudKit */
+ (NSArray<CKKSKey*>*)remoteKeys: (CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
return [self allWhere: @{@"state": SecCKKSProcessedStateRemote, @"ckzone":zoneID.zoneName} error:error];
}

/* Returns all keys marked 'local', i.e., processed in the past */
+ (NSArray<CKKSKey*>*)localKeys: (CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
return [self allWhere: @{@"state": SecCKKSProcessedStateLocal, @"ckzone":zoneID.zoneName} error:error];
}

- (bool)saveToDatabaseAsOnlyCurrentKeyForClassAndState: (NSError * __autoreleasing *) error {
self.currentkey = true;

// Find other keys for our key class
NSArray<CKKSKey*>* keys = [CKKSKey currentKeysForClass: self.keyclass state: self.state zoneID:self.zoneID error:error];
if(!keys) {
return false;
}

for(CKKSKey* key in keys) {
key.currentkey = false;
if(![key saveToDatabase: error]) {
return false;
}
}
if(![self saveToDatabase: error]) {
return false;
}

return true;
}

#pragma mark - CKRecord handling

- (void) setFromCKRecord: (CKRecord*) record {
if(![record.recordType isEqual: SecCKRecordIntermediateKeyType]) {
@throw [NSException
exceptionWithName:@"WrongCKRecordTypeException"
reason:[NSString stringWithFormat: @"CKRecordType (%@) was not %@", record.recordType, SecCKRecordIntermediateKeyType]
userInfo:nil];
}

[self setStoredCKRecord: record];

self.uuid = [[record recordID] recordName];
if(record[SecCKRecordParentKeyRefKey] != nil) {
self.parentKeyUUID = [record[SecCKRecordParentKeyRefKey] recordID].recordName;
} else {
// We wrap ourself.
self.parentKeyUUID = self.uuid;
}

self.keyclass = record[SecCKRecordKeyClassKey];
self.wrappedkey = [[CKKSWrappedAESSIVKey alloc] initWithBase64: record[SecCKRecordWrappedKeyKey]];
}

- (CKRecord*) updateCKRecord: (CKRecord*) record zoneID: (CKRecordZoneID*) zoneID {
if(![record.recordType isEqual: SecCKRecordIntermediateKeyType]) {
@throw [NSException
exceptionWithName:@"WrongCKRecordTypeException"
reason:[NSString stringWithFormat: @"CKRecordType (%@) was not %@", record.recordType, SecCKRecordIntermediateKeyType]
userInfo:nil];
}

// The parent key must exist in CloudKit, or this record save will fail.
if([self.parentKeyUUID isEqual: self.uuid]) {
// We wrap ourself. No parent.
record[SecCKRecordParentKeyRefKey] = nil;
} else {
record[SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: self.parentKeyUUID zoneID: zoneID] action: CKReferenceActionValidate];
}

[CKKSItem setOSVersionInRecord: record];

record[SecCKRecordKeyClassKey] = self.keyclass;
record[SecCKRecordWrappedKeyKey] = [self.wrappedkey base64WrappedKey];

return record;
}

- (bool)matchesCKRecord:(CKRecord*)record {
if(![record.recordType isEqual: SecCKRecordIntermediateKeyType]) {
return false;
}

if(![record.recordID.recordName isEqualToString: self.uuid]) {
secinfo("ckkskey", "UUID does not match");
return false;
}

// For the parent key ref, ensure that if it's nil, we wrap ourself
if(record[SecCKRecordParentKeyRefKey] == nil) {
if(![self wrapsSelf]) {
secinfo("ckkskey", "wrapping key reference (self-wrapped) does not match");
return false;
}

} else {
if(![[[record[SecCKRecordParentKeyRefKey] recordID] recordName] isEqualToString: self.parentKeyUUID]) {
secinfo("ckkskey", "wrapping key reference (non-self-wrapped) does not match");
return false;
}
}

if(![record[SecCKRecordKeyClassKey] isEqual: self.keyclass]) {
secinfo("ckkskey", "key class does not match");
return false;
}

if(![record[SecCKRecordWrappedKeyKey] isEqual: [self.wrappedkey base64WrappedKey]]) {
secinfo("ckkskey", "wrapped key does not match");
return false;
}

return true;
}

#pragma mark - Utility

- (NSString*)description {
return [NSString stringWithFormat: @"<%@(%@): %@ (%@,%@:%d)>",
NSStringFromClass([self class]),
self.zoneID.zoneName,
self.uuid,
self.keyclass,
self.state,
self.currentkey];
}

#pragma mark - CKKSSQLDatabaseObject methods

+ (NSString*) sqlTable {
return @"synckeys";
}

+ (NSArray<NSString*>*) sqlColumns {
return @[@"UUID", @"parentKeyUUID", @"ckzone", @"ckrecord", @"keyclass", @"state", @"currentkey", @"wrappedkey"];
}

- (NSDictionary<NSString*,NSString*>*) whereClauseToFindSelf {
return @{@"UUID": self.uuid, @"state": self.state, @"ckzone":self.zoneID.zoneName};
}

- (NSDictionary<NSString*,NSString*>*) sqlValues {
return @{@"UUID": self.uuid,
@"parentKeyUUID": self.parentKeyUUID ? self.parentKeyUUID : self.uuid, // if we don't have a parent, we wrap ourself.
@"ckzone": CKKSNilToNSNull(self.zoneID.zoneName),
@"ckrecord": CKKSNilToNSNull([self.encodedCKRecord base64EncodedStringWithOptions:0]),
@"keyclass": CKKSNilToNSNull(self.keyclass),
@"state": CKKSNilToNSNull(self.state),
@"wrappedkey": CKKSNilToNSNull([self.wrappedkey base64WrappedKey]),
@"currentkey": self.currentkey ? @"1" : @"0"};
}

+ (instancetype) fromDatabaseRow: (NSDictionary*) row {
return [[CKKSKey alloc] initWithWrappedAESKey: row[@"wrappedkey"] ? [[CKKSWrappedAESSIVKey alloc] initWithBase64: row[@"wrappedkey"]] : nil
uuid: row[@"UUID"]
parentKeyUUID: row[@"parentKeyUUID"]
keyclass: row[@"keyclass"]
state: row[@"state"]
zoneID: [[CKRecordZoneID alloc] initWithZoneName: row[@"ckzone"] ownerName:CKCurrentUserDefaultName]
encodedCKRecord: [[NSData alloc] initWithBase64EncodedString: row[@"ckrecord"] options:0]
currentkey: [row[@"currentkey"] integerValue]];

}

+ (NSDictionary<NSString*,NSNumber*>*)countsByClass:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
NSMutableDictionary* results = [[NSMutableDictionary alloc] init];

[CKKSSQLDatabaseObject queryDatabaseTable: [[self class] sqlTable]
where: @{@"ckzone": CKKSNilToNSNull(zoneID.zoneName)}
columns: @[@"keyclass", @"state", @"count(rowid)"]
groupBy: @[@"keyclass", @"state"]
orderBy:nil
limit: -1
processRow: ^(NSDictionary* row) {
results[[NSString stringWithFormat: @"%@-%@", row[@"state"], row[@"keyclass"]]] =
[NSNumber numberWithInteger: [row[@"count(rowid)"] integerValue]];
}
error: error];
return results;
}

- (instancetype)copyWithZone:(NSZone *)zone {
CKKSKey *keyCopy = [super copyWithZone:zone];
keyCopy->_aessivkey = _aessivkey;
keyCopy->_state = _state;
keyCopy->_keyclass = _keyclass;
keyCopy->_currentkey = _currentkey;
return keyCopy;
}

- (NSData*)serializeAsProtobuf: (NSError * __autoreleasing *) error {
if(![self ensureKeyLoaded:error]) {
return nil;
}
CKKSSerializedKey* proto = [[CKKSSerializedKey alloc] init];

proto.uuid = self.uuid;
proto.zoneName = self.zoneID.zoneName;
proto.keyclass = self.keyclass;
proto.key = [[NSData alloc] initWithBytes:self.aessivkey->key length:self.aessivkey->size];

return proto.data;
}

+ (CKKSKey*)loadFromProtobuf:(NSData*)data error:(NSError* __autoreleasing *)error {
CKKSSerializedKey* key = [[CKKSSerializedKey alloc] initWithData: data];
if(key && key.uuid && key.zoneName && key.keyclass && key.key) {
return [[CKKSKey alloc] initSelfWrappedWithAESKey:[[CKKSAESSIVKey alloc] initWithBytes:(uint8_t*)key.key.bytes len:key.key.length]
uuid:key.uuid
keyclass:(CKKSKeyClass*)key.keyclass // TODO sanitize
state:SecCKKSProcessedStateRemote
zoneID:[[CKRecordZoneID alloc] initWithZoneName:key.zoneName
ownerName:CKCurrentUserDefaultName]
encodedCKRecord:nil
currentkey:false];
}

if(error) {
*error = [NSError errorWithDomain:CKKSErrorDomain code:CKKSProtobufFailure description:@"Data failed to parse as a CKKSSerializedKey"];
}
return nil;
}

@end

#endif // OCTAGON