CKKSTLKShare.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/CKKSTLKShare.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 CKKSTLKShare ()
@end

@implementation CKKSTLKShare
-(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])) {
        _curve = curve;
        _version = version;
        _tlkUUID = key.uuid;

        _receiver = receiver;
        _senderPeerID = sender.peerID;

        _epoch = epoch;
        _poisoned = poisoned;
    }
    return self;
}

- (instancetype)initForKey:(NSString*)tlkUUID
              senderPeerID:(NSString*)senderPeerID
            recieverPeerID:(NSString*)receiverPeerID
      receiverEncPublicKey:(SFECPublicKey*)publicKey
                     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])) {
        _tlkUUID = tlkUUID;
        _senderPeerID = senderPeerID;

        _receiver = [[CKKSSOSPeer alloc] initWithSOSPeerID:receiverPeerID encryptionPublicKey:publicKey signingPublicKey:nil];

        _curve = curve;
        _version = version;
        _epoch = epoch;
        _poisoned = poisoned;

        _wrappedTLK = wrappedKey;
        _signature = signature;
    }
    return self;
}

- (NSString*)description {
    return [NSString stringWithFormat:@"<CKKSTLKShare(%@): recv:%@ send:%@>", self.tlkUUID, self.receiver, self.senderPeerID];
}

- (NSData*)wrap:(CKKSKey*)key publicKey:(SFECPublicKey*)receiverPublicKey error:(NSError* __autoreleasing *)error {
    NSData* plaintext = [key serializeAsProtobuf:error];
    if(!plaintext) {
        return nil;
    }

    SFIESOperation* sfieso = [[SFIESOperation alloc] initWithCurve:self.curve];
    SFIESCiphertext* ciphertext = [sfieso encrypt:plaintext withKey:receiverPublicKey error:error];

    // Now use NSCoding to turn the ciphertext into something transportable
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initRequiringSecureCoding:YES];
    [ciphertext encodeWithCoder:archiver];

    return archiver.encodedData;
}

- (CKKSKey*)unwrapUsing:(id<CKKSSelfPeer>)localPeer error:(NSError * __autoreleasing *)error {
    // Unwrap the ciphertext using NSSecureCoding
    NSKeyedUnarchiver *coder = [[NSKeyedUnarchiver alloc] initForReadingFromData:self.wrappedTLK error:nil];
    SFIESCiphertext* ciphertext = [[SFIESCiphertext alloc] initWithCoder:coder];
    [coder finishDecoding];

    SFIESOperation* sfieso = [[SFIESOperation alloc] initWithCurve:self.curve];

    NSError* localerror = nil;
    NSData* plaintext = [sfieso decrypt:ciphertext withKey:localPeer.encryptionKey error:&localerror];
    if(!plaintext || localerror) {
        if(error) {
            *error = localerror;
        }
        return nil;
    }

    return [CKKSKey loadFromProtobuf:plaintext error:error];
}

// Serialize this record in some format suitable for signing.
// This record must serialize exactly the same on the other side for the signature to verify.
- (NSData*)dataForSigning {
    // Ideally, we'd put this as DER or some other structured, versioned format.
    // For now, though, do the straightforward thing and concatenate the fields of interest.
    NSMutableData* dataToSign = [[NSMutableData alloc] init];

    uint64_t version = OSSwapHostToLittleConstInt64(self.version);
    [dataToSign appendBytes:&version length:sizeof(version)];

    // We only include the peer IDs in the signature; the receiver doesn't care if we signed the receiverPublicKey field;
    // if it's wrong or doesn't match, the receiver will simply fail to decrypt the encrypted record.
    [dataToSign appendData:[self.receiver.peerID dataUsingEncoding:NSUTF8StringEncoding]];
    [dataToSign appendData:[self.senderPeerID dataUsingEncoding:NSUTF8StringEncoding]];

    [dataToSign appendData:self.wrappedTLK];

    uint64_t curve = OSSwapHostToLittleConstInt64(self.curve);
    [dataToSign appendBytes:&curve length:sizeof(curve)];

    uint64_t epoch = OSSwapHostToLittleConstInt64(self.epoch);
    [dataToSign appendBytes:&epoch length:sizeof(epoch)];

    uint64_t poisoned = OSSwapHostToLittleConstInt64(self.poisoned);
    [dataToSign appendBytes:&poisoned length:sizeof(poisoned)];

    // If we have a CKRecord saved here, add any unknown fields (that don't start with server_) to the signed data
    // in sorted order by CKRecord key
    CKRecord* record = self.storedCKRecord;
    if(record) {
        NSMutableDictionary<NSString*,id>* extraData = [NSMutableDictionary dictionary];

        for(NSString* key in record.allKeys) {
            if([key isEqualToString:SecCKRecordSenderPeerID] ||
               [key isEqualToString:SecCKRecordReceiverPeerID] ||
               [key isEqualToString:SecCKRecordReceiverPublicEncryptionKey] ||
               [key isEqualToString:SecCKRecordCurve] ||
               [key isEqualToString:SecCKRecordEpoch] ||
               [key isEqualToString:SecCKRecordPoisoned] ||
               [key isEqualToString:SecCKRecordSignature] ||
               [key isEqualToString:SecCKRecordVersion] ||
               [key isEqualToString:SecCKRecordParentKeyRefKey] ||
               [key isEqualToString:SecCKRecordWrappedKeyKey]) {
                // This version of CKKS knows about this data field. Ignore them with prejudice.
                continue;
            }

            if([key hasPrefix:@"server_"]) {
                // Ignore all fields prefixed by "server_"
                continue;
            }

            extraData[key] = record[key];
        }

        NSArray* extraKeys = [[extraData allKeys] sortedArrayUsingSelector:@selector(compare:)];
        for(NSString* extraKey in extraKeys) {
            id obj = extraData[extraKey];

            // Skip CKReferences, NSArray, CLLocation, and CKAsset.
            if([obj isKindOfClass: [NSString class]]) {
                [dataToSign appendData: [obj dataUsingEncoding: NSUTF8StringEncoding]];
            } else if([obj isKindOfClass: [NSData class]]) {
                [dataToSign appendData: obj];
            } else if([obj isKindOfClass:[NSDate class]]) {
                NSISO8601DateFormatter *formatter = [[NSISO8601DateFormatter alloc] init];
                NSString* str = [formatter stringForObjectValue: obj];
                [dataToSign appendData: [str dataUsingEncoding: NSUTF8StringEncoding]];
            } else if([obj isKindOfClass: [NSNumber class]]) {
                // Add an NSNumber
                uint64_t n64 = OSSwapHostToLittleConstInt64([obj unsignedLongLongValue]);
                [dataToSign appendBytes:&n64 length:sizeof(n64)];
            }
        }
    }

    return dataToSign;
}

// Returns the signature, but not the signed data itself;
- (NSData*)signRecord:(SFECKeyPair*)signingKey error:(NSError* __autoreleasing *)error {
    // TODO: the digest operation can't be changed, as we don't have a good way of communicating it, like self.curve
    SFEC_X962SigningOperation* xso = [[SFEC_X962SigningOperation alloc] initWithKeySpecifier:[[SFECKeySpecifier alloc] initWithCurve:self.curve]
                                                                             digestOperation:[[SFSHA256DigestOperation alloc] init]];

    NSData* data = [self dataForSigning];
    SFSignedData* signedData = [xso sign:data withKey:signingKey error:error];

    return signedData.signature;
}

- (bool)verifySignature:(NSData*)signature verifyingPeer:(id<CKKSPeer>)peer error:(NSError* __autoreleasing *)error {
    if(!peer.publicSigningKey) {
        secerror("ckksshare: no signing key for peer: %@", peer);
        if(error) {
            *error = [NSError errorWithDomain:CKKSErrorDomain
                                         code:CKKSNoSigningKey
                                  description:[NSString stringWithFormat:@"Peer(%@) has no signing key", peer]];
        }
        return false;
    }

    // TODO: the digest operation can't be changed, as we don't have a good way of communicating it, like self.curve
    SFEC_X962SigningOperation* xso = [[SFEC_X962SigningOperation alloc] initWithKeySpecifier:[[SFECKeySpecifier alloc] initWithCurve:self.curve]
                                                                             digestOperation:[[SFSHA256DigestOperation alloc] init]];
    SFSignedData* signedData = [[SFSignedData alloc] initWithData:[self dataForSigning] signature:signature];

    bool ret = [xso verify:signedData withKey:peer.publicSigningKey error:error];
    return ret;
}

- (bool)signatureVerifiesWithPeerSet:(NSSet<id<CKKSPeer>>*)peerSet error:(NSError**)error {
    NSError* lastVerificationError = nil;
    for(id<CKKSPeer> peer in peerSet) {
        if([peer.peerID isEqualToString: self.senderPeerID]) {
            // Does the signature verify using this peer?
            NSError* localerror = nil;
            bool isSigned = [self verifySignature:self.signature verifyingPeer:peer error:&localerror];
            if(localerror) {
                secerror("ckksshare: signature didn't verify for %@ %@: %@", self, peer, localerror);
                lastVerificationError = localerror;
            }
            if(isSigned) {
                return true;
            }
        }
    }

    if(error) {
        if(lastVerificationError) {
            *error = lastVerificationError;
        } else {
            *error = [NSError errorWithDomain:CKKSErrorDomain
                                         code:CKKSNoTrustedTLKShares
                                  description:[NSString stringWithFormat:@"No TLK share from %@", self.senderPeerID]];
        }
    }
    return false;
}

- (instancetype)copyWithZone:(NSZone *)zone {
    CKKSTLKShare* share = [[[self class] allocWithZone:zone] init];
    share.curve = self.curve;
    share.version = self.version;
    share.tlkUUID = [self.tlkUUID copy];
    share.senderPeerID = [self.senderPeerID copy];
    share.epoch = self.epoch;
    share.poisoned = self.poisoned;
    share.wrappedTLK = [self.wrappedTLK copy];
    share.signature = [self.signature copy];

    share.receiver = self.receiver;
    return share;
}

- (BOOL)isEqual:(id)object {
    if(![object isKindOfClass:[CKKSTLKShare class]]) {
        return NO;
    }

    CKKSTLKShare* obj = (CKKSTLKShare*) object;

    // Note that for purposes of CKKSTLK equality, we only care about the receiver's peer ID and publicEncryptionKey
    // <rdar://problem/34897551> SFKeys should support [isEqual:]
    return ([self.tlkUUID isEqualToString:obj.tlkUUID] &&
            [self.zoneID isEqual: obj.zoneID] &&
            [self.senderPeerID isEqualToString:obj.senderPeerID] &&
            ((self.receiver.peerID == nil && obj.receiver.peerID == nil) || [self.receiver.peerID isEqual: obj.receiver.peerID]) &&
            ((self.receiver.publicEncryptionKey == nil && obj.receiver.publicEncryptionKey == nil)
                || [self.receiver.publicEncryptionKey.keyData isEqual: obj.receiver.publicEncryptionKey.keyData]) &&
            self.epoch == obj.epoch &&
            self.curve == obj.curve &&
            self.poisoned == obj.poisoned &&
            ((self.wrappedTLK == nil && obj.wrappedTLK == nil) || [self.wrappedTLK isEqual: obj.wrappedTLK]) &&
            ((self.signature == nil && obj.signature == nil) || [self.signature isEqual: obj.signature]) &&
            true) ? YES : NO;
}

+ (CKKSTLKShare*)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
    CKKSTLKShare* oldShare =  [CKKSTLKShare tryFromDatabase:key.uuid
                                            receiverPeerID:receiver.peerID
                                              senderPeerID:sender.peerID
                                                    zoneID:key.zoneID
                                                     error:&localerror];
    if(localerror) {
        secerror("ckksshare: couldn't load old share for %@: %@", key, localerror);
        if(error) {
            *error = localerror;
        }
        return nil;
    }

    CKKSTLKShare* share = [[CKKSTLKShare alloc] init:key
                                              sender:sender
                                            receiver:receiver
                                               curve:SFEllipticCurveNistp384
                                             version:SecCKKSTLKShareCurrentVersion
                                               epoch:epoch
                                            poisoned:poisoned
                                              zoneID:key.zoneID
                                     encodedCKRecord:oldShare.encodedCKRecord];

    share.wrappedTLK = [share wrap:key publicKey:receiver.publicEncryptionKey error:&localerror];
    if(localerror) {
        secerror("ckksshare: couldn't share %@ (wrap failed): %@", key, localerror);
        if(error) {
            *error = localerror;
        }
        return nil;
    }

    share.signature = [share signRecord:sender.signingKey error:&localerror];
    if(localerror) {
        secerror("ckksshare: couldn't share %@ (signing failed): %@", key, localerror);
        if(error) {
            *error = localerror;
        }
        return nil;
    }

    return share;
}

- (CKKSKey*)recoverTLK:(id<CKKSSelfPeer>)recoverer
          trustedPeers:(NSSet<id<CKKSPeer>>*)peers
                 error:(NSError* __autoreleasing *)error
{
    NSError* localerror = nil;

    id<CKKSPeer> peer = nil;
    for(id<CKKSPeer> p in peers) {
        if([p.peerID isEqualToString: self.senderPeerID]) {
            peer = p;
        }
    }

    if(!peer) {
        localerror = [NSError errorWithDomain:CKKSErrorDomain
                                         code:CKKSNoTrustedPeer
                                  description:[NSString stringWithFormat:@"No trusted peer signed %@", self]];
        if(error) {
            *error = localerror;
        }
        return nil;
    }

    bool isSigned = [self verifySignature:self.signature verifyingPeer:peer error:error];
    if(!isSigned) {
        return nil;
    }

    CKKSKey* tlkTrial = [self unwrapUsing:recoverer error:error];
    if(!tlkTrial) {
        return nil;
    }

    if(![self.tlkUUID isEqualToString:tlkTrial.uuid]) {
        localerror = [NSError errorWithDomain:CKKSErrorDomain
                                         code:CKKSDataMismatch
                                  description:[NSString stringWithFormat:@"Signed UUID doesn't match unsigned UUID for %@", self]];
        if(error) {
            *error = localerror;
        }
        return nil;
    }

    return tlkTrial;
}

#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<CKKSTLKShare*>*)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<CKKSTLKShare*>*)allForUUID:(NSString*)uuid
                               zoneID:(CKRecordZoneID*)zoneID
                                error:(NSError * __autoreleasing *)error {
    return [self allWhere:@{@"uuid":CKKSNilToNSNull(uuid),
                            @"ckzone":CKKSNilToNSNull(zoneID.zoneName)} error:error];
}

+ (NSArray<CKKSTLKShare*>*)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-%@::%@::%@", self.tlkUUID, self.receiver.peerID, self.senderPeerID];
}

- (CKRecord*)updateCKRecord:(CKRecord*)record zoneID:(CKRecordZoneID*)zoneID {
    if(![record.recordID.recordName isEqualToString: [self CKRecordName]]) {
        @throw [NSException
                exceptionWithName:@"WrongCKRecordNameException"
                reason:[NSString stringWithFormat: @"CKRecord name (%@) was not %@", record.recordID.recordName, [self CKRecordName]]
                userInfo:nil];
    }
    if(![record.recordType isEqualToString: SecCKRecordTLKShareType]) {
        @throw [NSException
                exceptionWithName:@"WrongCKRecordTypeException"
                reason:[NSString stringWithFormat: @"CKRecordType (%@) was not %@", record.recordType, SecCKRecordTLKShareType]
                userInfo:nil];
    }

    record[SecCKRecordSenderPeerID] = self.senderPeerID;
    record[SecCKRecordReceiverPeerID] = self.receiver.peerID;
    record[SecCKRecordReceiverPublicEncryptionKey] = [self.receiver.publicEncryptionKey.keyData base64EncodedStringWithOptions:0];
    record[SecCKRecordCurve] = [NSNumber numberWithUnsignedInteger:(NSUInteger)self.curve];
    record[SecCKRecordVersion] = [NSNumber numberWithUnsignedInteger:(NSUInteger)self.version];
    record[SecCKRecordEpoch] = [NSNumber numberWithLong:(long)self.epoch];
    record[SecCKRecordPoisoned] = [NSNumber numberWithLong:(long)self.poisoned];

    record[SecCKRecordParentKeyRefKey] = [[CKReference alloc] initWithRecordID: [[CKRecordID alloc] initWithRecordName: self.tlkUUID zoneID: zoneID]
                                                                        action: CKReferenceActionValidate];

    record[SecCKRecordWrappedKeyKey] = [self.wrappedTLK base64EncodedStringWithOptions:0];
    record[SecCKRecordSignature] = [self.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;
    }

    CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
    return [self isEqual: share];
}

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

    [self setStoredCKRecord:record];

    self.senderPeerID = record[SecCKRecordSenderPeerID];
    self.curve = [record[SecCKRecordCurve] longValue]; // TODO: sanitize
    self.version = [record[SecCKRecordVersion] longValue];

    NSData* pubkeydata = CKKSUnbase64NullableString(record[SecCKRecordReceiverPublicEncryptionKey]);
    NSError* error = nil;
    SFECPublicKey* receiverPublicKey = pubkeydata ? [[SFECPublicKey alloc] initWithData:pubkeydata
                                                                              specifier:[[SFECKeySpecifier alloc] initWithCurve:self.curve]
                                                                                  error:&error] : nil;

    if(error) {
        ckkserror("ckksshare", record.recordID.zoneID, "Couldn't make public key from data: %@", error);
        receiverPublicKey = nil;
    }

    self.receiver = [[CKKSSOSPeer alloc] initWithSOSPeerID:record[SecCKRecordReceiverPeerID] encryptionPublicKey:receiverPublicKey signingPublicKey:nil];

    self.epoch = [record[SecCKRecordEpoch] longValue];
    self.poisoned = [record[SecCKRecordPoisoned] longValue];

    self.tlkUUID = ((CKReference*)record[SecCKRecordParentKeyRefKey]).recordID.recordName;

    self.wrappedTLK = CKKSUnbase64NullableString(record[SecCKRecordWrappedKeyKey]);
    self.signature = CKKSUnbase64NullableString(record[SecCKRecordSignature]);
}

#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.tlkUUID,
             @"senderpeerid":self.senderPeerID,
             @"recvpeerid":self.receiver.peerID,
             @"ckzone":self.zoneID.zoneName,
             };
}

- (NSDictionary<NSString*,NSString*>*)sqlValues {
    return @{@"uuid":            self.tlkUUID,
             @"senderpeerid":    self.senderPeerID,
             @"recvpeerid":      self.receiver.peerID,
             @"recvpubenckey":   CKKSNilToNSNull([self.receiver.publicEncryptionKey.keyData base64EncodedStringWithOptions:0]),
             @"poisoned":        [NSString stringWithFormat:@"%ld", (long)self.poisoned],
             @"epoch":           [NSString stringWithFormat:@"%ld", (long)self.epoch],
             @"curve":           [NSString stringWithFormat:@"%ld", (long)self.curve],
             @"version":         [NSString stringWithFormat:@"%ld", (long)self.version],
             @"wrappedkey":      CKKSNilToNSNull([self.wrappedTLK      base64EncodedStringWithOptions:0]),
             @"signature":       CKKSNilToNSNull([self.signature       base64EncodedStringWithOptions:0]),
             @"ckzone":          CKKSNilToNSNull(self.zoneID.zoneName),
             @"ckrecord":        CKKSNilToNSNull([self.encodedCKRecord base64EncodedStringWithOptions:0]),
             };
}

+ (instancetype)fromDatabaseRow:(NSDictionary<NSString*,NSString*>*)row {
    CKRecordZoneID* zoneID = [[CKRecordZoneID alloc] initWithZoneName: row[@"ckzone"] ownerName:CKCurrentUserDefaultName];

    SFEllipticCurve curve = (SFEllipticCurve)[row[@"curve"] integerValue];  // TODO: sanitize
    SecCKKSTLKShareVersion version = (SecCKKSTLKShareVersion)[row[@"version"] integerValue]; // TODO: sanitize

    NSData* keydata = CKKSUnbase64NullableString(row[@"recvpubenckey"]);
    NSError* error = nil;
    SFECPublicKey* receiverPublicKey = keydata ? [[SFECPublicKey alloc] initWithData:keydata
                                                                           specifier:[[SFECKeySpecifier alloc] initWithCurve:curve]
                                                                               error:&error] : nil;

    if(error) {
        ckkserror("ckksshare", zoneID, "Couldn't make public key from data: %@", error);
        receiverPublicKey = nil;
    }

    return [[CKKSTLKShare alloc] initForKey:row[@"uuid"]
                               senderPeerID:row[@"senderpeerid"]
                             recieverPeerID:row[@"recvpeerid"]
                       receiverEncPublicKey:receiverPublicKey
                                      curve:curve
                                    version:version
                                      epoch:[row[@"epoch"] integerValue]
                                   poisoned:[row[@"poisoned"] integerValue]
                                 wrappedKey:CKKSUnbase64NullableString(row[@"wrappedkey"])
                                  signature:CKKSUnbase64NullableString(row[@"signature"])
                                     zoneID:zoneID
                            encodedCKRecord:CKKSUnbase64NullableString(row[@"ckrecord"])
            ];
}

@end

#endif // OCTAGON