]> git.saurik.com Git - apple/security.git/blobdiff - keychain/ckks/CKKSTLKShare.m
Security-58286.31.2.tar.gz
[apple/security.git] / keychain / ckks / CKKSTLKShare.m
diff --git a/keychain/ckks/CKKSTLKShare.m b/keychain/ckks/CKKSTLKShare.m
new file mode 100644 (file)
index 0000000..fd3b269
--- /dev/null
@@ -0,0 +1,616 @@
+/*
+ * 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 "keychain/ckks/CKKSTLKShare.h"
+#import "keychain/ckks/CKKSPeer.h"
+#import "keychain/ckks/CloudKitCategories.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.peerID, 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
+    NSMutableData* data = [NSMutableData data];
+    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
+    [ciphertext encodeWithCoder:archiver];
+    [archiver finishEncoding];
+
+    return data;
+}
+
+- (CKKSKey*)unwrapUsing:(id<CKKSSelfPeer>)localPeer error:(NSError * __autoreleasing *)error {
+    // Unwrap the ciphertext using NSSecureCoding
+    NSKeyedUnarchiver *coder = [[NSKeyedUnarchiver alloc] initForReadingWithData:self.wrappedTLK];
+    coder.requiresSecureCoding = YES;
+    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 {
+    // 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;
+}
+
+- (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