2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
26 #import <Foundation/NSKeyedArchiver_Private.h>
28 #import "keychain/ckks/CKKSTLKShareRecord.h"
29 #import "keychain/ckks/CKKSPeer.h"
30 #import "keychain/ckks/CloudKitCategories.h"
31 #import "keychain/categories/NSError+UsefulConstructors.h"
33 #import <SecurityFoundation/SFKey.h>
34 #import <SecurityFoundation/SFEncryptionOperation.h>
35 #import <SecurityFoundation/SFSigningOperation.h>
36 #import <SecurityFoundation/SFDigestOperation.h>
38 @interface CKKSTLKShareRecord ()
41 @implementation CKKSTLKShareRecord
43 - (instancetype)init:(CKKSTLKShare*)share
44 zoneID:(CKRecordZoneID*)zoneID
45 encodedCKRecord:(NSData*)encodedCKRecord
47 if((self = [super initWithCKRecordType:SecCKRecordTLKShareType
48 encodedCKRecord:encodedCKRecord
55 -(instancetype)init:(CKKSKey*)key
56 sender:(id<CKKSSelfPeer>)sender
57 receiver:(id<CKKSPeer>)receiver
58 curve:(SFEllipticCurve)curve
59 version:(SecCKKSTLKShareVersion)version
60 epoch:(NSInteger)epoch
61 poisoned:(NSInteger)poisoned
62 zoneID:(CKRecordZoneID*)zoneID
63 encodedCKRecord:(NSData*)encodedCKRecord
65 if((self = [super initWithCKRecordType:SecCKRecordTLKShareType
66 encodedCKRecord:encodedCKRecord
69 _share = [[CKKSTLKShare alloc] init:key.keycore
81 - (instancetype)initForKey:(NSString*)tlkUUID
82 senderPeerID:(NSString*)senderPeerID
83 recieverPeerID:(NSString*)receiverPeerID
84 receiverEncPublicKeySPKI:(NSData*)publicKeySPKI
85 curve:(SFEllipticCurve)curve
86 version:(SecCKKSTLKShareVersion)version
87 epoch:(NSInteger)epoch
88 poisoned:(NSInteger)poisoned
89 wrappedKey:(NSData*)wrappedKey
90 signature:(NSData*)signature
91 zoneID:(CKRecordZoneID*)zoneID
92 encodedCKRecord:(NSData*)encodedCKRecord
94 if((self = [super initWithCKRecordType:SecCKRecordTLKShareType
95 encodedCKRecord:encodedCKRecord
98 _share = [[CKKSTLKShare alloc] initForKey:tlkUUID
99 senderPeerID:senderPeerID
100 recieverPeerID:receiverPeerID
101 receiverEncPublicKeySPKI:publicKeySPKI
106 wrappedKey:wrappedKey
113 - (NSString*)description {
114 return [NSString stringWithFormat:@"<CKKSTLKShare(%@): recv:%@ send:%@>",
116 self.share.receiverPeerID,
117 self.share.senderPeerID];
122 return self.share.tlkUUID;
125 - (NSString*)senderPeerID
127 return self.share.senderPeerID;
131 return self.share.epoch;
134 - (NSInteger)poisoned
136 return self.share.poisoned;
139 - (NSData*)wrappedTLK
141 return self.share.wrappedTLK;
145 return self.share.signature;
148 - (CKKSKey*)unwrapUsing:(id<CKKSSelfPeer>)localPeer
149 error:(NSError * __autoreleasing *)error
151 CKKSKeychainBackedKey* realkey = [self.share unwrapUsing:localPeer
158 return [[CKKSKey alloc] initWithKeyCore:realkey];
161 - (NSData*)dataForSigning
163 return [self.share dataForSigning:self.storedCKRecord];
166 // Returns the signature, but not the signed data itself;
167 - (NSData*)signRecord:(SFECKeyPair*)signingKey
168 error:(NSError* __autoreleasing *)error
170 return [self.share signRecord:signingKey
171 ckrecord:self.storedCKRecord
175 - (bool)verifySignature:(NSData*)signature
176 verifyingPeer:(id<CKKSPeer>)peer
177 error:(NSError* __autoreleasing *)error
179 return [self.share verifySignature:signature
181 ckrecord:self.storedCKRecord
185 - (bool)signatureVerifiesWithPeerSet:(NSSet<id<CKKSPeer>>*)peerSet
186 error:(NSError**)error
188 return [self.share signatureVerifiesWithPeerSet:peerSet
189 ckrecord:self.storedCKRecord
193 - (instancetype)copyWithZone:(NSZone *)zone {
194 CKKSTLKShareRecord* shareRecord = [[[self class] allocWithZone:zone] init];
195 shareRecord.share = [self.share copyWithZone:zone];
199 - (BOOL)isEqual:(id)object {
200 if(![object isKindOfClass:[CKKSTLKShareRecord class]]) {
204 CKKSTLKShareRecord* obj = (CKKSTLKShareRecord*) object;
205 return [self.share isEqual: obj.share];
208 + (CKKSTLKShareRecord*)share:(CKKSKey*)key
209 as:(id<CKKSSelfPeer>)sender
210 to:(id<CKKSPeer>)receiver
211 epoch:(NSInteger)epoch
212 poisoned:(NSInteger)poisoned
213 error:(NSError* __autoreleasing *)error
215 NSError* localerror = nil;
216 // Load any existing TLK Share, so we can update it
217 CKKSTLKShareRecord* oldShare = [CKKSTLKShareRecord tryFromDatabase:key.uuid
218 receiverPeerID:receiver.peerID
219 senderPeerID:sender.peerID
223 ckkserror("ckksshare", key.zoneID, "couldn't load old share for %@: %@", key, localerror);
230 CKKSTLKShare* share = [CKKSTLKShare share:key.keycore
240 CKKSTLKShareRecord* sharerecord = [[CKKSTLKShareRecord alloc] init:share
242 encodedCKRecord:oldShare.encodedCKRecord];
246 - (CKKSKey*)recoverTLK:(id<CKKSSelfPeer>)recoverer
247 trustedPeers:(NSSet<id<CKKSPeer>>*)peers
248 error:(NSError* __autoreleasing *)error
250 CKKSKeychainBackedKey* realkey = [self.share recoverTLK:recoverer
252 ckrecord:self.storedCKRecord
257 return [[CKKSKey alloc] initWithKeyCore:realkey];
260 #pragma mark - Database Operations
262 + (instancetype)fromDatabase:(NSString*)uuid
263 receiverPeerID:(NSString*)receiverPeerID
264 senderPeerID:(NSString*)senderPeerID
265 zoneID:(CKRecordZoneID*)zoneID
266 error:(NSError * __autoreleasing *)error {
267 return [self fromDatabaseWhere: @{@"uuid":CKKSNilToNSNull(uuid),
268 @"recvpeerid":CKKSNilToNSNull(receiverPeerID),
269 @"senderpeerid":CKKSNilToNSNull(senderPeerID),
270 @"ckzone": CKKSNilToNSNull(zoneID.zoneName)} error:error];
273 + (instancetype)tryFromDatabase:(NSString*)uuid
274 receiverPeerID:(NSString*)receiverPeerID
275 senderPeerID:(NSString*)senderPeerID
276 zoneID:(CKRecordZoneID*)zoneID
277 error:(NSError * __autoreleasing *)error {
278 return [self tryFromDatabaseWhere: @{@"uuid":CKKSNilToNSNull(uuid),
279 @"recvpeerid":CKKSNilToNSNull(receiverPeerID),
280 @"senderpeerid":CKKSNilToNSNull(senderPeerID),
281 @"ckzone": CKKSNilToNSNull(zoneID.zoneName)} error:error];
284 + (NSArray<CKKSTLKShareRecord*>*)allFor:(NSString*)receiverPeerID
285 keyUUID:(NSString*)uuid
286 zoneID:(CKRecordZoneID*)zoneID
287 error:(NSError * __autoreleasing *)error {
288 return [self allWhere:@{@"recvpeerid":CKKSNilToNSNull(receiverPeerID),
290 @"ckzone": CKKSNilToNSNull(zoneID.zoneName)} error:error];
293 + (NSArray<CKKSTLKShareRecord*>*)allForUUID:(NSString*)uuid
294 zoneID:(CKRecordZoneID*)zoneID
295 error:(NSError * __autoreleasing *)error {
296 return [self allWhere:@{@"uuid":CKKSNilToNSNull(uuid),
297 @"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error:error];
300 + (NSArray<CKKSTLKShareRecord*>*)allInZone:(CKRecordZoneID*)zoneID
301 error:(NSError * __autoreleasing *)error {
302 return [self allWhere:@{@"ckzone": CKKSNilToNSNull(zoneID.zoneName)} error:error];
305 + (instancetype)tryFromDatabaseFromCKRecordID:(CKRecordID*)recordID
306 error:(NSError * __autoreleasing *)error {
307 // Welp. Try to parse!
308 NSError *localerror = NULL;
309 NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^tlkshare-(?<uuid>[0-9A-Fa-f-]*)::(?<receiver>.*)::(?<sender>.*)$"
310 options:NSRegularExpressionCaseInsensitive
319 NSTextCheckingResult* regexmatch = [regex firstMatchInString:recordID.recordName options:0 range:NSMakeRange(0, recordID.recordName.length)];
322 *error = [NSError errorWithDomain:CKKSErrorDomain
323 code:CKKSNoSuchRecord
324 description:[NSString stringWithFormat:@"Couldn't parse '%@' as a TLKShare ID", recordID.recordName]];
329 NSString* uuid = [recordID.recordName substringWithRange:[regexmatch rangeWithName:@"uuid"]];
330 NSString* receiver = [recordID.recordName substringWithRange:[regexmatch rangeWithName:@"receiver"]];
331 NSString* sender = [recordID.recordName substringWithRange:[regexmatch rangeWithName:@"sender"]];
333 return [self tryFromDatabaseWhere: @{@"uuid":CKKSNilToNSNull(uuid),
334 @"recvpeerid":CKKSNilToNSNull(receiver),
335 @"senderpeerid":CKKSNilToNSNull(sender),
336 @"ckzone": CKKSNilToNSNull(recordID.zoneID.zoneName)} error:error];
339 #pragma mark - CKKSCKRecordHolder methods
341 + (NSString*)ckrecordPrefix {
345 - (NSString*)CKRecordName {
346 return [NSString stringWithFormat:@"tlkshare-%@::%@::%@", self.share.tlkUUID, self.share.receiverPeerID, self.share.senderPeerID];
349 - (CKRecord*)updateCKRecord:(CKRecord*)record zoneID:(CKRecordZoneID*)zoneID {
350 if(![record.recordID.recordName isEqualToString: [self CKRecordName]]) {
352 exceptionWithName:@"WrongCKRecordNameException"
353 reason:[NSString stringWithFormat: @"CKRecord name (%@) was not %@", record.recordID.recordName, [self CKRecordName]]
356 if(![record.recordType isEqualToString: SecCKRecordTLKShareType]) {
358 exceptionWithName:@"WrongCKRecordTypeException"
359 reason:[NSString stringWithFormat: @"CKRecordType (%@) was not %@", record.recordType, SecCKRecordTLKShareType]
363 record[SecCKRecordSenderPeerID] = self.share.senderPeerID;
364 record[SecCKRecordReceiverPeerID] = self.share.receiverPeerID;
365 record[SecCKRecordReceiverPublicEncryptionKey] = [self.share.receiverPublicEncryptionKeySPKI base64EncodedStringWithOptions:0];
366 record[SecCKRecordCurve] = [NSNumber numberWithUnsignedInteger:(NSUInteger)self.share.curve];
367 record[SecCKRecordVersion] = [NSNumber numberWithUnsignedInteger:(NSUInteger)self.share.version];
368 record[SecCKRecordEpoch] = [NSNumber numberWithLong:(long)self.share.epoch];
369 record[SecCKRecordPoisoned] = [NSNumber numberWithLong:(long)self.share.poisoned];
371 record[SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: self.share.tlkUUID zoneID: zoneID]
372 action: CKReferenceActionValidate];
374 record[SecCKRecordWrappedKeyKey] = [self.share.wrappedTLK base64EncodedStringWithOptions:0];
375 record[SecCKRecordSignature] = [self.share.signature base64EncodedStringWithOptions:0];
380 - (bool)matchesCKRecord:(CKRecord*)record {
381 if(![record.recordType isEqualToString: SecCKRecordTLKShareType]) {
385 if(![record.recordID.recordName isEqualToString: [self CKRecordName]]) {
389 CKKSTLKShareRecord* share = [[CKKSTLKShareRecord alloc] initWithCKRecord:record];
390 return [self isEqual: share];
393 - (void)setFromCKRecord: (CKRecord*) record {
394 if(![record.recordType isEqualToString: SecCKRecordTLKShareType]) {
396 exceptionWithName:@"WrongCKRecordTypeException"
397 reason:[NSString stringWithFormat: @"CKRecordType (%@) was not %@", record.recordType, SecCKRecordDeviceStateType]
401 [self setStoredCKRecord:record];
403 NSData* pubkeydata = CKKSUnbase64NullableString(record[SecCKRecordReceiverPublicEncryptionKey]);
405 self.share = [[CKKSTLKShare alloc] initForKey:((CKReference*)record[SecCKRecordParentKeyRefKey]).recordID.recordName
406 senderPeerID:record[SecCKRecordSenderPeerID]
407 recieverPeerID:record[SecCKRecordReceiverPeerID]
408 receiverEncPublicKeySPKI:pubkeydata
409 curve:[record[SecCKRecordCurve] longValue] // TODO: sanitize
410 version:[record[SecCKRecordVersion] longValue]
411 epoch:[record[SecCKRecordEpoch] longValue]
412 poisoned:[record[SecCKRecordPoisoned] longValue]
413 wrappedKey:[[NSData alloc] initWithBase64EncodedString:record[SecCKRecordWrappedKeyKey] options:0]
414 signature:[[NSData alloc] initWithBase64EncodedString:record[SecCKRecordSignature] options:0]
415 zoneID:record.recordID.zoneID];
418 #pragma mark - CKKSSQLDatabaseObject methods
420 + (NSString*)sqlTable {
424 + (NSArray<NSString*>*)sqlColumns {
425 return @[@"ckzone", @"uuid", @"senderpeerid", @"recvpeerid", @"recvpubenckey", @"poisoned", @"epoch", @"curve", @"version", @"wrappedkey", @"signature", @"ckrecord"];
428 - (NSDictionary<NSString*,NSString*>*)whereClauseToFindSelf {
429 return @{@"uuid":self.share.tlkUUID,
430 @"senderpeerid":self.share.senderPeerID,
431 @"recvpeerid":self.share.receiverPeerID,
432 @"ckzone":self.zoneID.zoneName,
436 - (NSDictionary<NSString*,NSString*>*)sqlValues {
437 return @{@"uuid": self.share.tlkUUID,
438 @"senderpeerid": self.share.senderPeerID,
439 @"recvpeerid": self.share.receiverPeerID,
440 @"recvpubenckey": CKKSNilToNSNull([self.share.receiverPublicEncryptionKeySPKI base64EncodedStringWithOptions:0]),
441 @"poisoned": [NSString stringWithFormat:@"%ld", (long)self.share.poisoned],
442 @"epoch": [NSString stringWithFormat:@"%ld", (long)self.share.epoch],
443 @"curve": [NSString stringWithFormat:@"%ld", (long)self.share.curve],
444 @"version": [NSString stringWithFormat:@"%ld", (long)self.share.version],
445 @"wrappedkey": CKKSNilToNSNull([self.share.wrappedTLK base64EncodedStringWithOptions:0]),
446 @"signature": CKKSNilToNSNull([self.share.signature base64EncodedStringWithOptions:0]),
447 @"ckzone": CKKSNilToNSNull(self.zoneID.zoneName),
448 @"ckrecord": CKKSNilToNSNull([self.encodedCKRecord base64EncodedStringWithOptions:0]),
452 + (instancetype)fromDatabaseRow:(NSDictionary<NSString*, CKKSSQLResult*>*)row {
453 CKRecordZoneID* zoneID = [[CKRecordZoneID alloc] initWithZoneName: row[@"ckzone"].asString ownerName:CKCurrentUserDefaultName];
455 SFEllipticCurve curve = (SFEllipticCurve)row[@"curve"].asNSInteger; // TODO: sanitize
456 SecCKKSTLKShareVersion version = (SecCKKSTLKShareVersion)row[@"version"].asNSInteger; // TODO: sanitize
458 return [[CKKSTLKShareRecord alloc] initForKey:row[@"uuid"].asString
459 senderPeerID:row[@"senderpeerid"].asString
460 recieverPeerID:row[@"recvpeerid"].asString
461 receiverEncPublicKeySPKI:row[@"recvpubenckey"].asBase64DecodedData
464 epoch:row[@"epoch"].asNSInteger
465 poisoned:row[@"poisoned"].asNSInteger
466 wrappedKey:row[@"wrappedkey"].asBase64DecodedData
467 signature:row[@"signature"].asBase64DecodedData
469 encodedCKRecord:row[@"ckrecord"].asBase64DecodedData