CKKSTLKShareRecord.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 <Foundation/NSKeyedArchiver_Private.h>
#import "keychain/ckks/CKKSTLKShareRecord.h"
#import "keychain/ckks/CKKSPeer.h"
#import "keychain/ckks/CloudKitCategories.h"
#import "keychain/categories/NSError+UsefulConstructors.h"
#import <SecurityFoundation/SFKey.h>
#import <SecurityFoundation/SFEncryptionOperation.h>
#import <SecurityFoundation/SFSigningOperation.h>
#import <SecurityFoundation/SFDigestOperation.h>
@interface CKKSTLKShareRecord ()
@end
@implementation CKKSTLKShareRecord
- (instancetype)init:(CKKSTLKShare*)share
zoneID:(CKRecordZoneID*)zoneID
encodedCKRecord:(NSData*)encodedCKRecord
{
if((self = [super initWithCKRecordType:SecCKRecordTLKShareType
encodedCKRecord:encodedCKRecord
zoneID:zoneID])) {
_share = share;
}
return self;
}
-(instancetype)init:(CKKSKey*)key
sender:(id<CKKSSelfPeer>)sender
receiver:(id<CKKSPeer>)receiver
curve:(SFEllipticCurve)curve
version:(SecCKKSTLKShareVersion)version
epoch:(NSInteger)epoch
poisoned:(NSInteger)poisoned
zoneID:(CKRecordZoneID*)zoneID
encodedCKRecord:(NSData*)encodedCKRecord
{
if((self = [super initWithCKRecordType:SecCKRecordTLKShareType
encodedCKRecord:encodedCKRecord
zoneID:zoneID])) {
_share = [[CKKSTLKShare alloc] init:key.keycore
sender:sender
receiver:receiver
curve:curve
version:version
epoch:epoch
poisoned:poisoned
zoneID:zoneID];
}
return self;
}
- (instancetype)initForKey:(NSString*)tlkUUID
senderPeerID:(NSString*)senderPeerID
recieverPeerID:(NSString*)receiverPeerID
receiverEncPublicKeySPKI:(NSData*)publicKeySPKI
curve:(SFEllipticCurve)curve
version:(SecCKKSTLKShareVersion)version
epoch:(NSInteger)epoch
poisoned:(NSInteger)poisoned
wrappedKey:(NSData*)wrappedKey
signature:(NSData*)signature
zoneID:(CKRecordZoneID*)zoneID
encodedCKRecord:(NSData*)encodedCKRecord
{
if((self = [super initWithCKRecordType:SecCKRecordTLKShareType
encodedCKRecord:encodedCKRecord
zoneID:zoneID])) {
_share = [[CKKSTLKShare alloc] initForKey:tlkUUID
senderPeerID:senderPeerID
recieverPeerID:receiverPeerID
receiverEncPublicKeySPKI:publicKeySPKI
curve:curve
version:version
epoch:epoch
poisoned:poisoned
wrappedKey:wrappedKey
signature:signature
zoneID:zoneID];
}
return self;
}
- (NSString*)description {
return [NSString stringWithFormat:@"<CKKSTLKShare( self.share.tlkUUID,
self.share.receiverPeerID,
self.share.senderPeerID];
}
- (NSString*)tlkUUID
{
return self.share.tlkUUID;
}
- (NSString*)senderPeerID
{
return self.share.senderPeerID;
}
- (NSInteger)epoch
{
return self.share.epoch;
}
- (NSInteger)poisoned
{
return self.share.poisoned;
}
- (NSData*)wrappedTLK
{
return self.share.wrappedTLK;
}
- (NSData*)signature
{
return self.share.signature;
}
- (CKKSKey*)unwrapUsing:(id<CKKSSelfPeer>)localPeer
error:(NSError * __autoreleasing *)error
{
CKKSKeychainBackedKey* realkey = [self.share unwrapUsing:localPeer
error:error];
if(!realkey) {
return nil;
}
return [[CKKSKey alloc] initWithKeyCore:realkey];
}
- (NSData*)dataForSigning
{
return [self.share dataForSigning:self.storedCKRecord];
}
// Returns the signature, but not the signed data itself;
- (NSData*)signRecord:(SFECKeyPair*)signingKey
error:(NSError* __autoreleasing *)error
{
return [self.share signRecord:signingKey
ckrecord:self.storedCKRecord
error:error];
}
- (bool)verifySignature:(NSData*)signature
verifyingPeer:(id<CKKSPeer>)peer
error:(NSError* __autoreleasing *)error
{
return [self.share verifySignature:signature
verifyingPeer:peer
ckrecord:self.storedCKRecord
error:error];
}
- (bool)signatureVerifiesWithPeerSet:(NSSet<id<CKKSPeer>>*)peerSet
error:(NSError**)error
{
return [self.share signatureVerifiesWithPeerSet:peerSet
ckrecord:self.storedCKRecord
error:error];
}
- (instancetype)copyWithZone:(NSZone *)zone {
CKKSTLKShareRecord* shareRecord = [[[self class] allocWithZone:zone] init];
shareRecord.share = [self.share copyWithZone:zone];
return shareRecord;
}
- (BOOL)isEqual:(id)object {
if(![object isKindOfClass:[CKKSTLKShareRecord class]]) {
return NO;
}
CKKSTLKShareRecord* obj = (CKKSTLKShareRecord*) object;
return [self.share isEqual: obj.share];
}
+ (CKKSTLKShareRecord*)share:(CKKSKey*)key
as:(id<CKKSSelfPeer>)sender
to:(id<CKKSPeer>)receiver
epoch:(NSInteger)epoch
poisoned:(NSInteger)poisoned
error:(NSError* __autoreleasing *)error
{
NSError* localerror = nil;
// Load any existing TLK Share, so we can update it
CKKSTLKShareRecord* oldShare = [CKKSTLKShareRecord tryFromDatabase:key.uuid
receiverPeerID:receiver.peerID
senderPeerID:sender.peerID
zoneID:key.zoneID
error:&localerror];
if(localerror) {
ckkserror("ckksshare", key.zoneID, "couldn't load old share for if(error) {
*error = localerror;
}
return nil;
}
CKKSTLKShare* share = [CKKSTLKShare share:key.keycore
as:sender
to:receiver
epoch:epoch
poisoned:poisoned
error:error];
if(!share) {
return nil;
}
CKKSTLKShareRecord* sharerecord = [[CKKSTLKShareRecord alloc] init:share
zoneID:key.zoneID
encodedCKRecord:oldShare.encodedCKRecord];
return sharerecord;
}
- (CKKSKey*)recoverTLK:(id<CKKSSelfPeer>)recoverer
trustedPeers:(NSSet<id<CKKSPeer>>*)peers
error:(NSError* __autoreleasing *)error
{
CKKSKeychainBackedKey* realkey = [self.share recoverTLK:recoverer
trustedPeers:peers
ckrecord:self.storedCKRecord
error:error];
if(!realkey) {
return nil;
}
return [[CKKSKey alloc] initWithKeyCore:realkey];
}
#pragma mark - Database Operations
+ (instancetype)fromDatabase:(NSString*)uuid
receiverPeerID:(NSString*)receiverPeerID
senderPeerID:(NSString*)senderPeerID
zoneID:(CKRecordZoneID*)zoneID
error:(NSError * __autoreleasing *)error {
return [self fromDatabaseWhere: @{@"uuid":CKKSNilToNSNull(uuid),
@"recvpeerid":CKKSNilToNSNull(receiverPeerID),
@"senderpeerid":CKKSNilToNSNull(senderPeerID),
@"ckzone": CKKSNilToNSNull(zoneID.zoneName)} error:error];
}
+ (instancetype)tryFromDatabase:(NSString*)uuid
receiverPeerID:(NSString*)receiverPeerID
senderPeerID:(NSString*)senderPeerID
zoneID:(CKRecordZoneID*)zoneID
error:(NSError * __autoreleasing *)error {
return [self tryFromDatabaseWhere: @{@"uuid":CKKSNilToNSNull(uuid),
@"recvpeerid":CKKSNilToNSNull(receiverPeerID),
@"senderpeerid":CKKSNilToNSNull(senderPeerID),
@"ckzone": CKKSNilToNSNull(zoneID.zoneName)} error:error];
}
+ (NSArray<CKKSTLKShareRecord*>*)allFor:(NSString*)receiverPeerID
keyUUID:(NSString*)uuid
zoneID:(CKRecordZoneID*)zoneID
error:(NSError * __autoreleasing *)error {
return [self allWhere:@{@"recvpeerid":CKKSNilToNSNull(receiverPeerID),
@"uuid":uuid,
@"ckzone": CKKSNilToNSNull(zoneID.zoneName)} error:error];
}
+ (NSArray<CKKSTLKShareRecord*>*)allForUUID:(NSString*)uuid
zoneID:(CKRecordZoneID*)zoneID
error:(NSError * __autoreleasing *)error {
return [self allWhere:@{@"uuid":CKKSNilToNSNull(uuid),
@"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error:error];
}
+ (NSArray<CKKSTLKShareRecord*>*)allInZone:(CKRecordZoneID*)zoneID
error:(NSError * __autoreleasing *)error {
return [self allWhere:@{@"ckzone": CKKSNilToNSNull(zoneID.zoneName)} error:error];
}
+ (instancetype)tryFromDatabaseFromCKRecordID:(CKRecordID*)recordID
error:(NSError * __autoreleasing *)error {
// Welp. Try to parse!
NSError *localerror = NULL;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^tlkshare-(?<uuid>[0-9A-Fa-f-]*)::(?<receiver>.*)::(?<sender>.*)$"
options:NSRegularExpressionCaseInsensitive
error:&localerror];
if(localerror) {
if(error) {
*error = localerror;
}
return nil;
}
NSTextCheckingResult* regexmatch = [regex firstMatchInString:recordID.recordName options:0 range:NSMakeRange(0, recordID.recordName.length)];
if(!regexmatch) {
if(error) {
*error = [NSError errorWithDomain:CKKSErrorDomain
code:CKKSNoSuchRecord
description:[NSString stringWithFormat:@"Couldn't parse '%@' as a TLKShare ID", recordID.recordName]];
}
return nil;
}
NSString* uuid = [recordID.recordName substringWithRange:[regexmatch rangeWithName:@"uuid"]];
NSString* receiver = [recordID.recordName substringWithRange:[regexmatch rangeWithName:@"receiver"]];
NSString* sender = [recordID.recordName substringWithRange:[regexmatch rangeWithName:@"sender"]];
return [self tryFromDatabaseWhere: @{@"uuid":CKKSNilToNSNull(uuid),
@"recvpeerid":CKKSNilToNSNull(receiver),
@"senderpeerid":CKKSNilToNSNull(sender),
@"ckzone": CKKSNilToNSNull(recordID.zoneID.zoneName)} error:error];
}
#pragma mark - CKKSCKRecordHolder methods
+ (NSString*)ckrecordPrefix {
return @"tlkshare";
}
- (NSString*)CKRecordName {
return [NSString stringWithFormat:@"tlkshare-}
- (CKRecord*)updateCKRecord:(CKRecord*)record zoneID:(CKRecordZoneID*)zoneID {
if(![record.recordID.recordName isEqualToString: [self CKRecordName]]) {
@throw [NSException
exceptionWithName:@"WrongCKRecordNameException"
reason:[NSString stringWithFormat: @"CKRecord name ( userInfo:nil];
}
if(![record.recordType isEqualToString: SecCKRecordTLKShareType]) {
@throw [NSException
exceptionWithName:@"WrongCKRecordTypeException"
reason:[NSString stringWithFormat: @"CKRecordType ( userInfo:nil];
}
record[SecCKRecordSenderPeerID] = self.share.senderPeerID;
record[SecCKRecordReceiverPeerID] = self.share.receiverPeerID;
record[SecCKRecordReceiverPublicEncryptionKey] = [self.share.receiverPublicEncryptionKeySPKI base64EncodedStringWithOptions:0];
record[SecCKRecordCurve] = [NSNumber numberWithUnsignedInteger:(NSUInteger)self.share.curve];
record[SecCKRecordVersion] = [NSNumber numberWithUnsignedInteger:(NSUInteger)self.share.version];
record[SecCKRecordEpoch] = [NSNumber numberWithLong:(long)self.share.epoch];
record[SecCKRecordPoisoned] = [NSNumber numberWithLong:(long)self.share.poisoned];
record[SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: self.share.tlkUUID zoneID: zoneID]
action: CKReferenceActionValidate];
record[SecCKRecordWrappedKeyKey] = [self.share.wrappedTLK base64EncodedStringWithOptions:0];
record[SecCKRecordSignature] = [self.share.signature base64EncodedStringWithOptions:0];
return record;
}
- (bool)matchesCKRecord:(CKRecord*)record {
if(![record.recordType isEqualToString: SecCKRecordTLKShareType]) {
return false;
}
if(![record.recordID.recordName isEqualToString: [self CKRecordName]]) {
return false;
}
CKKSTLKShareRecord* share = [[CKKSTLKShareRecord alloc] initWithCKRecord:record];
return [self isEqual: share];
}
- (void)setFromCKRecord: (CKRecord*) record {
if(![record.recordType isEqualToString: SecCKRecordTLKShareType]) {
@throw [NSException
exceptionWithName:@"WrongCKRecordTypeException"
reason:[NSString stringWithFormat: @"CKRecordType ( userInfo:nil];
}
[self setStoredCKRecord:record];
NSData* pubkeydata = CKKSUnbase64NullableString(record[SecCKRecordReceiverPublicEncryptionKey]);
self.share = [[CKKSTLKShare alloc] initForKey:((CKReference*)record[SecCKRecordParentKeyRefKey]).recordID.recordName
senderPeerID:record[SecCKRecordSenderPeerID]
recieverPeerID:record[SecCKRecordReceiverPeerID]
receiverEncPublicKeySPKI:pubkeydata
curve:[record[SecCKRecordCurve] longValue] // TODO: sanitize
version:[record[SecCKRecordVersion] longValue]
epoch:[record[SecCKRecordEpoch] longValue]
poisoned:[record[SecCKRecordPoisoned] longValue]
wrappedKey:[[NSData alloc] initWithBase64EncodedString:record[SecCKRecordWrappedKeyKey] options:0]
signature:[[NSData alloc] initWithBase64EncodedString:record[SecCKRecordSignature] options:0]
zoneID:record.recordID.zoneID];
}
#pragma mark - CKKSSQLDatabaseObject methods
+ (NSString*)sqlTable {
return @"tlkshare";
}
+ (NSArray<NSString*>*)sqlColumns {
return @[@"ckzone", @"uuid", @"senderpeerid", @"recvpeerid", @"recvpubenckey", @"poisoned", @"epoch", @"curve", @"version", @"wrappedkey", @"signature", @"ckrecord"];
}
- (NSDictionary<NSString*,NSString*>*)whereClauseToFindSelf {
return @{@"uuid":self.share.tlkUUID,
@"senderpeerid":self.share.senderPeerID,
@"recvpeerid":self.share.receiverPeerID,
@"ckzone":self.zoneID.zoneName,
};
}
- (NSDictionary<NSString*,NSString*>*)sqlValues {
return @{@"uuid": self.share.tlkUUID,
@"senderpeerid": self.share.senderPeerID,
@"recvpeerid": self.share.receiverPeerID,
@"recvpubenckey": CKKSNilToNSNull([self.share.receiverPublicEncryptionKeySPKI base64EncodedStringWithOptions:0]),
@"poisoned": [NSString stringWithFormat:@" @"epoch": [NSString stringWithFormat:@" @"curve": [NSString stringWithFormat:@" @"version": [NSString stringWithFormat:@" @"wrappedkey": CKKSNilToNSNull([self.share.wrappedTLK base64EncodedStringWithOptions:0]),
@"signature": CKKSNilToNSNull([self.share.signature base64EncodedStringWithOptions:0]),
@"ckzone": CKKSNilToNSNull(self.zoneID.zoneName),
@"ckrecord": CKKSNilToNSNull([self.encodedCKRecord base64EncodedStringWithOptions:0]),
};
}
+ (instancetype)fromDatabaseRow:(NSDictionary<NSString*, CKKSSQLResult*>*)row {
CKRecordZoneID* zoneID = [[CKRecordZoneID alloc] initWithZoneName: row[@"ckzone"].asString ownerName:CKCurrentUserDefaultName];
SFEllipticCurve curve = (SFEllipticCurve)row[@"curve"].asNSInteger; // TODO: sanitize
SecCKKSTLKShareVersion version = (SecCKKSTLKShareVersion)row[@"version"].asNSInteger; // TODO: sanitize
return [[CKKSTLKShareRecord alloc] initForKey:row[@"uuid"].asString
senderPeerID:row[@"senderpeerid"].asString
recieverPeerID:row[@"recvpeerid"].asString
receiverEncPublicKeySPKI:row[@"recvpubenckey"].asBase64DecodedData
curve:curve
version:version
epoch:row[@"epoch"].asNSInteger
poisoned:row[@"poisoned"].asNSInteger
wrappedKey:row[@"wrappedkey"].asBase64DecodedData
signature:row[@"signature"].asBase64DecodedData
zoneID:zoneID
encodedCKRecord:row[@"ckrecord"].asBase64DecodedData
];
}
+ (BOOL)intransactionRecordChanged:(CKRecord*)record resync:(BOOL)resync error:(NSError**)error
{
NSError* localerror = nil;
// CKKSTLKShares get saved with no modification
CKKSTLKShareRecord* share = [[CKKSTLKShareRecord alloc] initWithCKRecord:record];
bool saved = [share saveToDatabase:&localerror];
if(!saved || localerror) {
ckkserror("ckksshare", record.recordID.zoneID, "Couldn't save new TLK share to database: if(error) {
*error = localerror;
}
return NO;
}
return YES;
}
+ (BOOL)intransactionRecordDeleted:(CKRecordID*)recordID resync:(BOOL)resync error:(NSError**)error
{
NSError* localerror = nil;
ckksinfo("ckksshare", recordID.zoneID, "CloudKit notification: deleted tlk share record(
CKKSTLKShareRecord* share = [CKKSTLKShareRecord tryFromDatabaseFromCKRecordID:recordID error:&localerror];
[share deleteFromDatabase:&localerror];
if(localerror) {
ckkserror("ckksshare", recordID.zoneID, "CK notification: Couldn't delete deleted TLKShare: if(error) {
*error = localerror;
}
return NO;
}
return YES;
}
+ (NSNumber* _Nullable)counts:(CKRecordZoneID*)zoneID error:(NSError * __autoreleasing *)error
{
__block NSNumber *result = nil;
[CKKSSQLDatabaseObject queryDatabaseTable:[[self class] sqlTable]
where:@{@"ckzone": CKKSNilToNSNull(zoneID.zoneName)}
columns:@[@"count(rowid)"]
groupBy:nil
orderBy:nil
limit:-1
processRow:^(NSDictionary<NSString*, CKKSSQLResult*>* row) {
result = row[@"count(rowid)"].asNSNumberInteger;
}
error: error];
return result;
}
@end
#endif // OCTAGON