CKKSOutgoingQueueEntry.m [plain text]
/*
* Copyright (c) 2016 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@
*/
#include <AssertMacros.h>
#import <Foundation/Foundation.h>
#import "CKKSKeychainView.h"
#include <Security/SecItemPriv.h>
#include <utilities/SecDb.h>
#include "keychain/securityd/SecDbItem.h"
#include "keychain/securityd/SecItemSchema.h"
#if OCTAGON
#import <CloudKit/CloudKit.h>
#import "CKKSOutgoingQueueEntry.h"
#import "CKKSItemEncrypter.h"
#import "CKKSKey.h"
#import "keychain/ckks/CloudKitCategories.h"
#import "keychain/categories/NSError+UsefulConstructors.h"
#import "keychain/ckks/CKKSViewManager.h"
@implementation CKKSOutgoingQueueEntry
- (NSString*)description {
return [NSString stringWithFormat: @"< NSStringFromClass([self class]),
self.item.zoneID.zoneName,
self.action,
self.item.uuid,
self.state];
}
- (instancetype) initWithCKKSItem:(CKKSItem*) item
action: (NSString*) action
state: (NSString*) state
waitUntil: (NSDate*) waitUntil
accessGroup: (NSString*) accessgroup
{
if((self = [super init])) {
_item = item;
_action = action;
_state = state;
_accessgroup = accessgroup;
_waitUntil = waitUntil;
}
return self;
}
- (BOOL)isEqual: (id) object {
if(![object isKindOfClass:[CKKSOutgoingQueueEntry class]]) {
return NO;
}
CKKSOutgoingQueueEntry* obj = (CKKSOutgoingQueueEntry*) object;
return ([self.item isEqual: obj.item] &&
[self.action isEqual: obj.action] &&
[self.state isEqual: obj.state] &&
((self.waitUntil == nil && obj.waitUntil == nil) || (fabs([self.waitUntil timeIntervalSinceDate: obj.waitUntil]) < 1)) &&
[self.accessgroup isEqual: obj.accessgroup] &&
true) ? YES : NO;
}
+ (CKKSKey*)keyForItem:(SecDbItemRef)item
zoneID:(CKRecordZoneID*)zoneID
keyCache:(CKKSMemoryKeyCache* _Nullable)keyCache
error:(NSError * __autoreleasing *)error
{
if(!item) {
ckkserror("ckks-key", zoneID, "Cannot select a key for no item!");
if(error) {
*error = [NSError errorWithDomain:CKKSErrorDomain
code:CKKSErrorUnexpectedNil
description:@"can't pick a key class for an empty item"];
}
return nil;
}
CKKSKeyClass* class = nil;
NSString* protection = (__bridge NSString*)SecDbItemGetCachedValueWithName(item, kSecAttrAccessible);
if([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleWhenUnlocked]) {
class = SecCKKSKeyClassA;
} else if([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAlwaysPrivate] ||
[protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAfterFirstUnlock]) {
class = SecCKKSKeyClassC;
} else {
NSError* localError = [NSError errorWithDomain:CKKSErrorDomain
code:CKKSInvalidKeyClass
description:[NSString stringWithFormat:@"can't pick key class for protection ckkserror("ckks-key", zoneID, "can't pick key class: if(error) {
*error = localError;
}
return nil;
}
NSError* currentKeyError = nil;
CKKSKey* key = nil;
if(keyCache) {
key = [keyCache currentKeyForClass:class zoneID:zoneID error:¤tKeyError];
} else {
key = [CKKSKey currentKeyForClass:class zoneID:zoneID error:¤tKeyError];
}
if(!key || currentKeyError) {
ckkserror("ckks-key", zoneID, "Couldn't find current key for
if(error) {
*error = currentKeyError;
}
return nil;
}
// and make sure it's unwrapped.
NSError* loadedError = nil;
if(![key ensureKeyLoaded:&loadedError]) {
ckkserror("ckks-key", zoneID, "Couldn't load key( if(error) {
*error = loadedError;
}
return nil;
}
return key;
}
+ (instancetype)withItem:(SecDbItemRef)item
action:(NSString*)action
zoneID:(CKRecordZoneID*)zoneID
keyCache:(CKKSMemoryKeyCache* _Nullable)keyCache
error:(NSError * __autoreleasing *)error
{
CFErrorRef cferror = NULL;
CKKSKey* key = nil;
NSString* uuid = nil;
NSString* accessgroup = nil;
NSInteger newGenerationCount = -1;
NSMutableDictionary* objd = nil;
ckkserror("ckksitem", zoneID, "Creating a (
NSError* keyError = nil;
key = [self keyForItem:item
zoneID:zoneID
keyCache:keyCache
error:&keyError];
if(!key || keyError) {
NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:keyError.code description:@"No key for item" underlying:keyError];
ckkserror("ckksitem", zoneID, "no key for item: if(error) {
*error = localerror;
}
return nil;
}
objd = (__bridge_transfer NSMutableDictionary*) SecDbItemCopyPListWithMask(item, kSecDbSyncFlag, &cferror);
if(!objd) {
NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:CFErrorGetCode(cferror) description:@"Couldn't create object plist" underlying:(__bridge_transfer NSError*)cferror];
ckkserror("ckksitem", zoneID, "no plist: if(error) {
*error = localerror;
}
return nil;
}
// Object classes aren't in the item plist, set them specifically
[objd setObject: (__bridge NSString*) item->class->name forKey: (__bridge NSString*) kSecClass];
uuid = (__bridge_transfer NSString*) CFRetainSafe(SecDbItemGetValue(item, &v10itemuuid, &cferror));
if(!uuid || cferror) {
NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:CKKSNoUUIDOnItem description:@"No UUID for item" underlying:(__bridge_transfer NSError*)cferror];
ckkserror("ckksitem", zoneID, "No UUID for item: if(error) {
*error = localerror;
}
return nil;
}
if([uuid isKindOfClass:[NSNull class]]) {
NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:CKKSNoUUIDOnItem description:@"UUID not found in object" underlying:nil];
ckkserror("ckksitem", zoneID, "couldn't fetch UUID: if(error) {
*error = localerror;
}
return nil;
}
accessgroup = (__bridge_transfer NSString*) CFRetainSafe(SecDbItemGetValue(item, &v6agrp, &cferror));
if(!accessgroup || cferror) {
NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:CFErrorGetCode(cferror) description:@"accessgroup not found in object" underlying:(__bridge_transfer NSError*)cferror];
ckkserror("ckksitem", zoneID, "couldn't fetch access group from item: if(error) {
*error = localerror;
}
return nil;
}
if([accessgroup isKindOfClass:[NSNull class]]) {
// That's okay; this is only used for rate limiting.
ckkserror("ckksitem", zoneID, "couldn't fetch accessgroup: accessgroup = @"no-group";
}
CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:uuid zoneID:zoneID error:error];
// The action this change should be depends on any existing pending action, if any
// Particularly, we need to coalesce (existing action, new action) to:
// (add, modify) => add
// (add, delete) => no-op
// (delete, add) => modify
NSString* actualAction = action;
NSError* fetchError = nil;
CKKSOutgoingQueueEntry* existingOQE = [CKKSOutgoingQueueEntry tryFromDatabase:uuid state:SecCKKSStateNew zoneID:zoneID error:&fetchError];
if(existingOQE) {
if([existingOQE.action isEqual: SecCKKSActionAdd]) {
if([action isEqual:SecCKKSActionModify]) {
actualAction = SecCKKSActionAdd;
} else if([action isEqual:SecCKKSActionDelete]) {
// we're deleting an add. If there's a ckme, keep as a delete
if(!ckme) {
// Otherwise, remove from outgoingqueue and don't make a new OQE.
[existingOQE deleteFromDatabase:error];
return nil;
}
}
}
if([existingOQE.action isEqual: SecCKKSActionDelete] && [action isEqual:SecCKKSActionAdd]) {
actualAction = SecCKKSActionModify;
}
} else if(fetchError) {
ckkserror("ckksitem", zoneID, "Unable to fetch an existing OQE due to error: fetchError = nil;
} else {
if(!ckme && [action isEqualToString:SecCKKSActionDelete]) {
CKKSOutgoingQueueEntry* anyExistingOQE = [CKKSOutgoingQueueEntry tryFromDatabase:uuid zoneID:zoneID error:&fetchError];
if(fetchError) {
ckkserror("ckksitem", zoneID, "Unable to fetch an existing OQE (any state) due to error: } else if(!anyExistingOQE) {
// This is a delete for an item which doesn't exist. Therefore, this is a no-op.
ckkserror("ckksitem", zoneID, "Asked to delete a record for which we don't have a CKME or any OQE, ignoring: return nil;
}
}
}
newGenerationCount = ckme ? ckme.item.generationCount : (NSInteger) 0; // TODO: this is wrong
// Is this modification just changing the mdat? As a performance improvement, don't update the item in CK
if(ckme && !existingOQE && [actualAction isEqualToString:SecCKKSActionModify]) {
NSError* ckmeError = nil;
NSMutableDictionary* mirror = [[CKKSItemEncrypter decryptItemToDictionary:ckme.item keyCache:keyCache error:&ckmeError] mutableCopy];
NSMutableDictionary* objdCopy = [objd mutableCopy];
if(ckmeError) {
ckkserror("ckksitem", zoneID, "Unable to decrypt current CKME: } else {
mirror[(__bridge id)kSecAttrModificationDate] = nil;
mirror[(__bridge id)kSecAttrSHA1] = nil;
objdCopy[(__bridge id)kSecAttrModificationDate] = nil;
objdCopy[(__bridge id)kSecAttrSHA1] = nil;
if([mirror isEqualToDictionary:objdCopy]) {
ckksnotice("ckksitem", zoneID, "Update to item only changes mdat; skipping return nil;
}
}
}
// Pull out any unencrypted fields
NSNumber* pcsServiceIdentifier = objd[(id)kSecAttrPCSPlaintextServiceIdentifier];
objd[(id)kSecAttrPCSPlaintextServiceIdentifier] = nil;
NSData* pcsPublicKey = objd[(id)kSecAttrPCSPlaintextPublicKey];
objd[(id)kSecAttrPCSPlaintextPublicKey] = nil;
NSData* pcsPublicIdentity = objd[(id)kSecAttrPCSPlaintextPublicIdentity];
objd[(id)kSecAttrPCSPlaintextPublicIdentity] = nil;
CKKSItem* baseitem = [[CKKSItem alloc] initWithUUID:uuid
parentKeyUUID:key.uuid
zoneID:zoneID
encodedCKRecord:nil
encItem:nil
wrappedkey:nil
generationCount:newGenerationCount
encver:currentCKKSItemEncryptionVersion
plaintextPCSServiceIdentifier:pcsServiceIdentifier
plaintextPCSPublicKey:pcsPublicKey
plaintextPCSPublicIdentity:pcsPublicIdentity];
if(!baseitem) {
NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:CKKSItemCreationFailure description:@"Couldn't create an item" underlying:nil];
ckkserror("ckksitem", zoneID, "couldn't create an item: if(error) {
*error = localerror;
}
return nil;
}
NSError* encryptionError = nil;
CKKSItem* encryptedItem = [CKKSItemEncrypter encryptCKKSItem:baseitem
dataDictionary:objd
updatingCKKSItem:ckme.item
parentkey:key
keyCache:keyCache
error:&encryptionError];
if(!encryptedItem || encryptionError) {
NSError* localerror = [NSError errorWithDomain:CKKSErrorDomain code:encryptionError.code description:@"Couldn't encrypt item" underlying:encryptionError];
ckkserror("ckksitem", zoneID, "couldn't encrypt item: if(error) {
*error = localerror;
}
return nil;
}
return [[CKKSOutgoingQueueEntry alloc] initWithCKKSItem:encryptedItem
action:actualAction
state:SecCKKSStateNew
waitUntil:nil
accessGroup:accessgroup];
}
#pragma mark - Property access to underlying CKKSItem
-(NSString*)uuid {
return self.item.uuid;
}
-(void)setUuid:(NSString *)uuid {
self.item.uuid = uuid;
}
#pragma mark - Database Operations
+ (instancetype) fromDatabase: (NSString*) uuid state: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
return [self fromDatabaseWhere: @{@"UUID": CKKSNilToNSNull(uuid), @"state": CKKSNilToNSNull(state), @"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error: error];
}
+ (instancetype) tryFromDatabase: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
return [self tryFromDatabaseWhere: @{@"UUID": CKKSNilToNSNull(uuid), @"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error: error];
}
+ (instancetype) tryFromDatabase: (NSString*) uuid state: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
return [self tryFromDatabaseWhere: @{@"UUID": CKKSNilToNSNull(uuid), @"state":CKKSNilToNSNull(state), @"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error: error];
}
+ (NSArray<CKKSOutgoingQueueEntry*>*) fetch:(ssize_t) n state: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
return [self fetch:n where: @{@"state":CKKSNilToNSNull(state), @"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error:error];
}
+ (NSArray<CKKSOutgoingQueueEntry*>*) allInState: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
return [self allWhere: @{@"state":CKKSNilToNSNull(state), @"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error:error];
}
+ (NSArray<CKKSOutgoingQueueEntry*>*)allWithUUID:(NSString*)uuid states:(NSArray<NSString*>*)states zoneID:(CKRecordZoneID*)zoneID error:(NSError * __autoreleasing *)error
{
return [self allWhere:@{@"UUID": CKKSNilToNSNull(uuid),
@"state": [[CKKSSQLWhereIn alloc] initWithValues:states],
@"ckzone":CKKSNilToNSNull(zoneID.zoneName)}
error:error];
}
#pragma mark - CKKSSQLDatabaseObject methods
+ (NSString*)sqlTable {
return @"outgoingqueue";
}
+ (NSArray<NSString*>*)sqlColumns {
return [[CKKSItem sqlColumns] arrayByAddingObjectsFromArray: @[@"action", @"state", @"waituntil", @"accessgroup"]];
}
- (NSDictionary<NSString*,NSString*>*)whereClauseToFindSelf {
return @{@"UUID": self.uuid, @"state": self.state, @"ckzone":self.item.zoneID.zoneName};
}
- (NSDictionary<NSString*,NSString*>*)sqlValues {
NSISO8601DateFormatter* dateFormat = [[NSISO8601DateFormatter alloc] init];
NSMutableDictionary* values = [[self.item sqlValues] mutableCopy];
values[@"action"] = self.action;
values[@"state"] = self.state;
values[@"waituntil"] = CKKSNilToNSNull(self.waitUntil ? [dateFormat stringFromDate: self.waitUntil] : nil);
values[@"accessgroup"] = self.accessgroup;
return values;
}
+ (instancetype)fromDatabaseRow:(NSDictionary<NSString *, CKKSSQLResult*>*) row {
return [[CKKSOutgoingQueueEntry alloc] initWithCKKSItem:[CKKSItem fromDatabaseRow: row]
action:row[@"action"].asString
state:row[@"state"].asString
waitUntil:row[@"waituntil"].asISO8601Date
accessGroup:row[@"accessgroup"].asString];
}
+ (NSDictionary<NSString*,NSNumber*>*)countsByStateInZone:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
NSMutableDictionary* results = [[NSMutableDictionary alloc] init];
[CKKSSQLDatabaseObject queryDatabaseTable: [[self class] sqlTable]
where: @{@"ckzone": CKKSNilToNSNull(zoneID.zoneName)}
columns: @[@"state", @"count(rowid)"]
groupBy: @[@"state"]
orderBy:nil
limit: -1
processRow: ^(NSDictionary<NSString*, CKKSSQLResult*>* row) {
results[row[@"state"].asString] = row[@"count(rowid)"].asNSNumberInteger;
}
error: error];
return results;
}
+ (NSInteger)countByState:(CKKSItemState *)state zone:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error {
__block NSInteger result = -1;
[CKKSSQLDatabaseObject queryDatabaseTable: [[self class] sqlTable]
where: @{@"ckzone": CKKSNilToNSNull(zoneID.zoneName), @"state": state }
columns: @[@"count(*)"]
groupBy: nil
orderBy: nil
limit: -1
processRow: ^(NSDictionary<NSString*, CKKSSQLResult*>* row) {
result = row[@"count(*)"].asNSInteger;
}
error: error];
return result;
}
#pragma mark - State Transitions
- (BOOL)intransactionMoveToState:(NSString*)state
viewState:(CKKSKeychainViewState*)viewState
error:(NSError**)error
{
NSError* localerror = nil;
if([state isEqualToString:SecCKKSStateDeleted]) {
// Hurray, this must be a success
SecBoolNSErrorCallback syncCallback = [[CKKSViewManager manager] claimCallbackForUUID:self.uuid];
if(syncCallback) {
syncCallback(true, nil);
}
[self deleteFromDatabase:&localerror];
if(localerror) {
ckkserror("ckks", viewState, "Couldn't delete }
} else if([self.state isEqualToString:SecCKKSStateInFlight] && [state isEqualToString:SecCKKSStateNew]) {
// An in-flight OQE is moving to new? See if it's been superceded
CKKSOutgoingQueueEntry* newOQE = [CKKSOutgoingQueueEntry tryFromDatabase:self.uuid
state:SecCKKSStateNew
zoneID:viewState.zoneID
error:&localerror];
if(localerror) {
ckkserror("ckksoutgoing", viewState, "Couldn't fetch an overwriting OQE, assuming one doesn't exist: newOQE = nil;
}
if(newOQE) {
ckksnotice("ckksoutgoing", viewState, "New modification has come in behind inflight // recurse for that lovely code reuse
[self intransactionMoveToState:SecCKKSStateDeleted
viewState:viewState
error:&localerror];
if(localerror) {
ckkserror("ckksoutgoing", viewState, "Couldn't delete in-flight OQE: }
} else {
self.state = state;
[self saveToDatabase:&localerror];
if(localerror) {
ckkserror("ckks", viewState, "Couldn't save }
}
} else {
self.state = state;
[self saveToDatabase: &localerror];
if(localerror) {
ckkserror("ckks", viewState, "Couldn't save }
}
if(error && localerror) {
*error = localerror;
}
return localerror == nil;
}
- (BOOL)intransactionMarkAsError:(NSError*)itemError
viewState:(CKKSKeychainViewState*)viewState
error:(NSError* __autoreleasing*)error
{
SecBoolNSErrorCallback callback = [[CKKSViewManager manager] claimCallbackForUUID:self.uuid];
if(callback) {
callback(false, itemError);
}
NSError* localerror = nil;
// Now, delete the OQE: it's never coming back
[self deleteFromDatabase:&localerror];
if(localerror) {
ckkserror("ckks", viewState, "Couldn't delete }
if(error && localerror) {
*error = localerror;
}
return localerror == nil;
}
@end
#endif