// // MLSignalStore.m // Monal // // Created by Anurodh Pokharel on 5/3/18. // Copyright © 2018 Monal.im. All rights reserved. // #import "MLConstants.h" #import "MLSignalStore.h" #import "SignalProtocolObjC.h" #import "DataLayer.h" #import "MLSQLite.h" #import "HelperTools.h" @interface MLSignalStore() { NSString* _dbPath; } @property (nonatomic, strong) NSNumber* accountID; @property (readonly, strong) MLSQLite* sqliteDatabase; @end @implementation MLSignalStore +(void) initialize { //TODO: WE USE THE SAME DATABASE FILE AS THE DataLayer --> this should probably be migrated into the datalayer or use its own sqlite database //make sure the datalayer has migrated the database file to the app group location first [DataLayer initialize]; } //this is the getter of our readonly "sqliteDatabase" property always returning the thread-local instance of the MLSQLite class -(MLSQLite*) sqliteDatabase { //always return thread-local instance of sqlite class (this is important for performance!) return [MLSQLite sharedInstanceForFile:_dbPath]; } -(MLSignalStore*) initWithAccountID:(NSNumber*) accountID andAccountJid:(NSString* _Nonnull) accountJid { self = [super init]; _dbPath = [[HelperTools getContainerURLForPathComponents:@[@"sworim.sqlite"]] path]; self.accountID = accountID; NSArray* data = [self.sqliteDatabase idReadTransaction:^{ return [self.sqliteDatabase executeReader:@"SELECT identityPrivateKey, deviceid, identityPublicKey FROM signalIdentity WHERE account_id=?;" andArguments:@[accountID]]; }]; NSDictionary* row = [data firstObject]; if(row) { self.deviceid = [(NSNumber *)[row objectForKey:@"deviceid"] unsignedIntValue]; self.accountJid = accountJid; NSData* idKeyPub = [row objectForKey:@"identityPublicKey"]; NSData* idKeyPrivate = [row objectForKey:@"identityPrivateKey"]; NSError* error; self.identityKeyPair = [[SignalIdentityKeyPair alloc] initWithPublicKey:idKeyPub privateKey:idKeyPrivate error:nil]; if(error) { DDLogError(@"prekey error %@", error); return self; } self.signedPreKey = [[SignalSignedPreKey alloc] initWithSerializedData:[self loadSignedPreKeyWithId:1] error:&error]; if(error) { DDLogError(@"signed prekey error %@", error); return self; } // remove old keys that should no longer be available [self cleanupKeys]; [self reloadCachedPrekeys]; } else self.deviceid = 0; return self; } -(void) reloadCachedPrekeys { self.preKeys = [self readPreKeys]; } -(void) cleanupKeys { [self.sqliteDatabase voidWriteTransaction:^{ // remove old keys that have been remove a long time ago from pubsub [self.sqliteDatabase executeNonQuery:@"DELETE FROM signalPreKey WHERE account_id=? AND pubSubRemovalTimestamp IS NOT NULL AND pubSubRemovalTimestamp <= unixepoch('now', '-14 day');" andArguments:@[self.accountID]]; // mark old unused keys to be removed from pubsub [self.sqliteDatabase executeNonQuery:@"UPDATE signalPreKey SET pubSubRemovalTimestamp=CURRENT_TIMESTAMP WHERE account_id=? AND keyUsed=0 AND pubSubRemovalTimestamp IS NULL AND creationTimestamp<= unixepoch('now','-14 day');" andArguments:@[self.accountID]]; }]; } -(NSMutableArray*) readPreKeys { NSArray* keys = [self.sqliteDatabase idReadTransaction:^{ return [self.sqliteDatabase executeReader:@"SELECT prekeyid, preKey FROM signalPreKey WHERE account_id=? AND keyUsed=0;" andArguments:@[self.accountID]]; }]; NSMutableArray* array = [[NSMutableArray alloc] initWithCapacity:keys.count]; for (NSDictionary* row in keys) { SignalPreKey* key = [[SignalPreKey alloc] initWithSerializedData:[row objectForKey:@"preKey"] error:nil]; [array addObject:key]; } return array; } -(int) getHighestPreyKeyId { NSNumber* highestId = [self.sqliteDatabase idReadTransaction:^{ return [self.sqliteDatabase executeScalar:@"SELECT prekeyid FROM signalPreKey WHERE account_id=? ORDER BY prekeyid DESC LIMIT 1;" andArguments:@[self.accountID]]; }]; if(highestId == nil) { return 0; // Default value -> first preKeyId will be 1 } else { return highestId.intValue; } } -(unsigned int) getPreKeyCount { NSNumber* count = [self.sqliteDatabase idReadTransaction:^{ return [self.sqliteDatabase executeScalar:@"SELECT COUNT(prekeyid) FROM signalPreKey WHERE account_id=? AND pubSubRemovalTimestamp IS NULL AND keyUsed=0;" andArguments:@[self.accountID]]; }]; return count.unsignedIntValue; } -(void) saveValues { [self storeSignedPreKey:self.signedPreKey.serializedData signedPreKeyId:1]; [self storeIdentityPublicKey:self.identityKeyPair.publicKey andPrivateKey:self.identityKeyPair.privateKey]; for (SignalPreKey *key in self.preKeys) { [self storePreKey:key.serializedData preKeyId:key.preKeyId]; } [self reloadCachedPrekeys]; } /** * Returns a copy of the serialized session record corresponding to the * provided recipient ID + device ID tuple. * or nil if not found. */ - (NSData* _Nullable) sessionRecordForAddress:(SignalAddress*) address { return [self.sqliteDatabase idReadTransaction:^{ return [self.sqliteDatabase executeScalar:@"SELECT recordData FROM signalContactSession WHERE account_id=? AND contactName=? AND contactDeviceId=?;" andArguments:@[self.accountID, address.name, [NSNumber numberWithInteger:address.deviceId]]]; }]; } /** * Commit to storage the session record for a given * recipient ID + device ID tuple. * * Return YES on success, NO on failure. */ -(BOOL) storeSessionRecord:(NSData*) recordData forAddress:(SignalAddress*) address { return [self.sqliteDatabase boolWriteTransaction:^{ if([self sessionRecordForAddress:address]) return [self.sqliteDatabase executeNonQuery:@"UPDATE signalContactSession SET recordData=? WHERE account_id=? AND contactName=? AND contactDeviceId=?;" andArguments:@[recordData, self.accountID, address.name, [NSNumber numberWithInteger:address.deviceId]]]; else return [self.sqliteDatabase executeNonQuery:@"INSERT INTO signalContactSession (account_id, contactName, contactDeviceId, recordData) VALUES (?, ?, ?, ?);" andArguments:@[self.accountID, address.name, [NSNumber numberWithInteger:address.deviceId], recordData]]; }]; } /** * Determine whether there is a committed session record for a * recipient ID + device ID tuple. */ - (BOOL) sessionRecordExistsForAddress:(SignalAddress*) address; { NSData* preKeyData = [self sessionRecordForAddress:address]; return preKeyData ? YES : NO; } /** * Remove a session record for a recipient ID + device ID tuple. */ - (BOOL) deleteSessionRecordForAddress:(SignalAddress*) address { return [self.sqliteDatabase boolWriteTransaction:^{ return [self.sqliteDatabase executeNonQuery:@"DELETE FROM signalContactSession WHERE account_id=? AND contactName=? AND contactDeviceId=?;" andArguments:@[self.accountID, address.name, [NSNumber numberWithInteger:address.deviceId]]]; }]; } /** * Returns all known devices with active sessions for a recipient */ - (NSArray*) allDeviceIdsForAddressName:(NSString*) jid { return [self.sqliteDatabase idReadTransaction:^{ return [self.sqliteDatabase executeScalarReader:@"SELECT DISTINCT contactDeviceId FROM signalContactSession WHERE account_id=? AND contactName=?;" andArguments:@[self.accountID, jid]]; }]; } -(NSArray*) knownDevicesForAddressName:(NSString*) jid { if(!jid) return nil; return [self.sqliteDatabase idReadTransaction:^{ return [self.sqliteDatabase executeScalarReader:@"SELECT DISTINCT contactDeviceId FROM signalContactIdentity WHERE account_id=? AND contactName=? AND removedFromDeviceList IS NULL;" andArguments:@[self.accountID, jid]]; }]; } -(NSArray*) knownDevicesWithValidSession:(NSString*) jid { return [self.sqliteDatabase idReadTransaction:^{ return [self.sqliteDatabase executeScalarReader:@"\ SELECT DISTINCT \ contactDeviceId \ FROM signalContactIdentity \ WHERE \ account_id=? \ AND contactName=? \ AND removedFromDeviceList IS NULL \ AND brokenSession=false \ ;" andArguments:@[self.accountID, jid]]; }]; } -(NSArray*) knownDevicesWithPendingBrokenSessionHandling:(NSString*) jid { return [self.sqliteDatabase idReadTransaction:^{ return [self.sqliteDatabase executeScalarReader:@"\ SELECT DISTINCT \ contactDeviceId \ FROM signalContactIdentity \ WHERE \ account_id=? \ AND contactName=? \ AND removedFromDeviceList IS NULL \ AND brokenSession=true \ AND (lastFailedBundleFetch IS NULL OR lastFailedBundleFetch <= unixepoch('now', '-5 day'))\ ;" andArguments:@[self.accountID, jid]]; }]; } /** * Remove the session records corresponding to all devices of a recipient ID. * * @return the number of deleted sessions on success, negative on failure */ -(int) deleteAllSessionsForAddressName:(NSString*) addressName { return [[self.sqliteDatabase idWriteTransaction:^{ NSNumber* count = (NSNumber*) [self.sqliteDatabase executeScalar:@"COUNT * FROM signalContactSession WHERE account_id=? AND contactName=?;" andArguments:@[self.accountID, addressName]]; [self.sqliteDatabase executeNonQuery:@"DELETE FROM signalContactSession WHERE account_id=? AND contactName=?;" andArguments:@[self.accountID, addressName]]; return count; }] intValue]; } /** * Load a local serialized PreKey record. * return nil if not found */ - (nullable NSData*) loadPreKeyWithId:(uint32_t) preKeyId; { DDLogDebug(@"Loading prekey %lu", (unsigned long)preKeyId); return [self.sqliteDatabase idReadTransaction:^{ return [self.sqliteDatabase executeScalar:@"SELECT prekey FROM signalPreKey WHERE account_id=? AND prekeyid=? AND keyUsed=0;" andArguments:@[self.accountID, [NSNumber numberWithInteger:preKeyId]]]; }]; } /** * Store a local serialized PreKey record. * return YES if storage successful, else NO */ -(BOOL) storePreKey:(NSData*) preKey preKeyId:(uint32_t) preKeyId { DDLogDebug(@"Storing prekey %lu", (unsigned long)preKeyId); return [self.sqliteDatabase boolWriteTransaction:^{ // Only store new preKeys NSNumber* preKeyCnt = [self.sqliteDatabase executeScalar:@"SELECT count(*) FROM signalPreKey WHERE account_id=? AND prekeyid=? AND preKey=?;" andArguments:@[self.accountID, [NSNumber numberWithInteger:preKeyId], preKey]]; if(preKeyCnt.intValue > 0) return YES; return [self.sqliteDatabase executeNonQuery:@"INSERT INTO signalPreKey (account_id, prekeyid, preKey) VALUES (?, ?, ?);" andArguments:@[self.accountID, [NSNumber numberWithInteger:preKeyId], preKey]]; }]; } /** * Determine whether there is a committed PreKey record matching the * provided ID. */ -(BOOL) containsPreKeyWithId:(uint32_t) preKeyId; { NSData* prekey = [self loadPreKeyWithId:preKeyId]; return prekey ? YES : NO; } /** * Delete a PreKey record from local storage. */ -(BOOL) deletePreKeyWithId:(uint32_t) preKeyId { DDLogDebug(@"Marking prekey %lu as deleted", (unsigned long)preKeyId); // only mark the key for deletion -> key should be removed from pubSub return [self.sqliteDatabase boolWriteTransaction:^{ BOOL ret = [self.sqliteDatabase executeNonQuery:@"UPDATE signalPreKey SET pubSubRemovalTimestamp=CURRENT_TIMESTAMP, keyUsed=1 WHERE account_id=? AND prekeyid=?;" andArguments:@[self.accountID, [NSNumber numberWithInteger:preKeyId]]]; [self reloadCachedPrekeys]; return ret; }]; } /** * Load a local serialized signed PreKey record. */ -(nullable NSData*) loadSignedPreKeyWithId:(uint32_t) signedPreKeyId { DDLogDebug(@"Loading signed prekey %lu", (unsigned long)signedPreKeyId); return [self.sqliteDatabase idReadTransaction:^{ return [self.sqliteDatabase executeScalar:@"SELECT signedPreKey FROM signalSignedPreKey WHERE account_id=? AND signedPreKeyId=?;" andArguments:@[self.accountID, [NSNumber numberWithInteger:signedPreKeyId]]]; }]; } /** * Store a local serialized signed PreKey record. */ - (BOOL) storeSignedPreKey:(NSData*) signedPreKey signedPreKeyId:(uint32_t) signedPreKeyId { DDLogDebug(@"Storing signed prekey %lu", (unsigned long)signedPreKeyId); return [self.sqliteDatabase boolWriteTransaction:^{ return [self.sqliteDatabase executeNonQuery:@"INSERT INTO signalSignedPreKey (account_id, signedPreKeyId, signedPreKey) VALUES (?, ?, ?) ON CONFLICT(account_id, signedPreKeyId) DO UPDATE SET signedPreKey=?;" andArguments:@[self.accountID, [NSNumber numberWithInteger:signedPreKeyId], signedPreKey, signedPreKey]]; }]; } /** * Determine whether there is a committed signed PreKey record matching * the provided ID. */ - (BOOL) containsSignedPreKeyWithId:(uint32_t) signedPreKeyId { return [self.sqliteDatabase idReadTransaction:^{ return [self.sqliteDatabase executeScalar:@"SELECT signedPreKey FROM signalSignedPreKey WHERE account_id=? AND signedPreKeyId=?;" andArguments:@[self.accountID, [NSNumber numberWithInteger:signedPreKeyId]]]; }] ? YES : NO; } /** * Delete a SignedPreKeyRecord from local storage. */ - (BOOL) removeSignedPreKeyWithId:(uint32_t) signedPreKeyId { DDLogDebug(@"Removing signed prekey %lu", (unsigned long)signedPreKeyId); return [self.sqliteDatabase boolWriteTransaction:^{ return [self.sqliteDatabase executeNonQuery:@"DELETE FROM signalSignedPreKey WHERE account_id=? AND signedPreKeyId=?;" andArguments:@[self.accountID, [NSNumber numberWithInteger:signedPreKeyId]]]; }]; } /** * Get the local client's identity key pair. */ -(SignalIdentityKeyPair*) getIdentityKeyPair; { return self.identityKeyPair; } -(BOOL) identityPublicKeyExists:(NSData*) publicKey andPrivateKey:(NSData *) privateKey { return [[self.sqliteDatabase idReadTransaction:^{ return [self.sqliteDatabase executeScalar:@"SELECT COUNT(*) FROM signalIdentity WHERE account_id=? AND deviceid=? AND identityPublicKey=? AND identityPrivateKey=?;" andArguments:@[self.accountID, [NSNumber numberWithUnsignedInt:self.deviceid], publicKey, privateKey]]; }] boolValue]; } - (BOOL) storeIdentityPublicKey:(NSData*) publicKey andPrivateKey:(NSData*) privateKey { return [self.sqliteDatabase boolWriteTransaction:^{ if([self identityPublicKeyExists:publicKey andPrivateKey:privateKey]) return YES; return [self.sqliteDatabase executeNonQuery:@"INSERT INTO signalIdentity (account_id, deviceid, identityPublicKey, identityPrivateKey) values (?, ?, ?, ?);" andArguments:@[self.accountID, [NSNumber numberWithInteger:self.deviceid], publicKey, privateKey]]; }]; } /** * Return the local client's registration ID. * * Clients should maintain a registration ID, a random number * between 1 and 16380 that's generated once at install time. * * return negative on failure */ - (uint32_t) getLocalRegistrationId; { return self.deviceid; } /** * Save a remote client's identity key *

* Store a remote client's identity key as trusted. * The value of key_data may be null. In this case remove the key data * from the identity store, but retain any metadata that may be kept * alongside it. */ -(BOOL) saveIdentity:(SignalAddress* _Nonnull) address identityKey:(NSData* _Nullable) identityKey; { return [self.sqliteDatabase boolWriteTransaction:^{ NSData* dbIdentity= (NSData *)[self.sqliteDatabase executeScalar:@"SELECT IDENTITY FROM signalContactIdentity WHERE account_id=? AND contactDeviceId=? AND contactName=?;" andArguments:@[self.accountID, [NSNumber numberWithInteger:address.deviceId], address.name]]; if(dbIdentity) return YES; // Set every new identity to TOFU to increase user experience return [self.sqliteDatabase executeNonQuery:@"INSERT INTO signalContactIdentity \ (account_id, contactName, contactDeviceId, identity, trustLevel) \ VALUES (?, ?, ?, ?, ?)" andArguments:@[self.accountID, address.name, [NSNumber numberWithInteger:address.deviceId], identityKey, [NSNumber numberWithInt:MLOmemoInternalToFU]]]; }]; } /** * Verify a remote client's identity key. * * Determine whether a remote client's identity is trusted. Convention is * that the TextSecure protocol is 'trust on first use.' This means that * an identity key is considered 'trusted' if there is no entry for the recipient * in the local store, or if it matches the saved key for a recipient in the local * store. Only if it mismatches an entry in the local store is it considered * 'untrusted.' */ -(BOOL) isTrustedIdentity:(SignalAddress*) address identityKey:(NSData*) identityKey; { int trustLevel = [self getTrustLevel:address identityKey:identityKey].intValue; // For better UX trust ToFU devices even if we did not receive msg in a long time return (trustLevel == MLOmemoTrusted || trustLevel == MLOmemoToFU || trustLevel == MLOmemoToFUButNoMsgSeenInTime); } -(NSData*) getIdentityForAddress:(SignalAddress*) address { return [self.sqliteDatabase idReadTransaction:^{ return [self.sqliteDatabase executeScalar:@"SELECT identity FROM signalContactIdentity WHERE account_id=? AND contactDeviceId=? AND contactName=?;" andArguments:@[self.accountID, [NSNumber numberWithInteger:address.deviceId], address.name]]; }]; } /* * ToFU independent trust update * true -> trust * false -> don't trust * -> after calling updateTrust once ToFU will be over */ -(void) updateTrust:(BOOL) trust forAddress:(SignalAddress*) address { [self.sqliteDatabase voidWriteTransaction:^{ [self.sqliteDatabase executeNonQuery:@"UPDATE signalContactIdentity SET trustLevel=? WHERE account_id=? AND contactDeviceId=? AND contactName=?;" andArguments:@[[NSNumber numberWithInt:(trust ? MLOmemoInternalTrusted : MLOmemoInternalNotTrusted)], self.accountID, [NSNumber numberWithInteger:address.deviceId], address.name]]; }]; } -(void) markDeviceAsDeleted:(SignalAddress*) address { [self.sqliteDatabase voidWriteTransaction:^{ [self.sqliteDatabase executeNonQuery:@"UPDATE signalContactIdentity SET removedFromDeviceList=CURRENT_TIMESTAMP WHERE account_id=? AND contactDeviceId=? AND contactName=?;" andArguments:@[self.accountID, [NSNumber numberWithInteger:address.deviceId], address.name]]; }]; } -(void) removeDeviceDeletedMark:(SignalAddress*) address { [self.sqliteDatabase voidWriteTransaction:^{ [self.sqliteDatabase executeNonQuery:@"UPDATE signalContactIdentity SET removedFromDeviceList=NULL WHERE account_id=? AND contactDeviceId=? AND contactName=?;" andArguments:@[self.accountID, [NSNumber numberWithInteger:address.deviceId], address.name]]; }]; } /* * update lastReceivedMsg to CURRENT_TIMESTAMP * reset brokenSession to false */ -(void) updateLastSuccessfulDecryptTime:(SignalAddress*) address { [self.sqliteDatabase voidWriteTransaction:^{ [self.sqliteDatabase executeNonQuery:@"UPDATE signalContactIdentity SET lastReceivedMsg=CURRENT_TIMESTAMP, brokenSession=false WHERE account_id=? AND contactDeviceId=? AND contactName=?;" andArguments:@[self.accountID, [NSNumber numberWithInteger:address.deviceId], address.name]]; }]; } -(void) markSessionAsBroken:(SignalAddress*) address { [self.sqliteDatabase voidWriteTransaction:^{ [self.sqliteDatabase executeNonQuery:@"UPDATE signalContactIdentity SET brokenSession=true WHERE account_id=? AND contactDeviceId=? AND contactName=?;" andArguments:@[self.accountID, [NSNumber numberWithInteger:address.deviceId], address.name]]; }]; } -(void) markBundleAsFixed:(SignalAddress*) address { [self.sqliteDatabase voidWriteTransaction:^{ [self.sqliteDatabase executeNonQuery:@"UPDATE signalContactIdentity SET brokenSession=false, lastFailedBundleFetch=NULL WHERE account_id=? AND contactDeviceId=? AND contactName=?;" andArguments:@[self.accountID, [NSNumber numberWithInteger:address.deviceId], address.name]]; }]; } -(void) markBundleAsBroken:(SignalAddress*) address { [self.sqliteDatabase voidWriteTransaction:^{ [self.sqliteDatabase executeNonQuery:@"UPDATE signalContactIdentity SET lastFailedBundleFetch=unixepoch('now') WHERE account_id=? AND contactDeviceId=? AND contactName=?;" andArguments:@[self.accountID, [NSNumber numberWithInteger:address.deviceId], address.name]]; }]; } -(BOOL) isSessionBrokenForJid:(NSString*) jid andDeviceId:(NSNumber*) deviceId { return [self.sqliteDatabase boolReadTransaction:^{ return [[self.sqliteDatabase executeScalar:@"SELECT brokenSession FROM signalContactIdentity WHERE account_id=? AND contactDeviceId=? AND contactName=?;" andArguments:@[self.accountID, deviceId, jid]] boolValue]; }]; } -(int) getInternalTrustLevel:(SignalAddress*) address identityKey:(NSData*) identityKey { return [[self.sqliteDatabase idReadTransaction:^{ return [self.sqliteDatabase executeScalar:(@"SELECT trustLevel FROM signalContactIdentity WHERE account_id=? AND contactDeviceId=? AND contactName=? AND identity=?;") andArguments:@[self.accountID, [NSNumber numberWithInteger:address.deviceId], address.name, identityKey]]; }] intValue]; } -(void) untrustAllDevicesFrom:(NSString*) jid { if([jid isEqualToString:self.accountJid] == NO) { // untrust all devices [self.sqliteDatabase voidWriteTransaction:^{ [self.sqliteDatabase executeNonQuery:@"UPDATE signalContactIdentity SET trustLevel=? WHERE account_id=? AND contactName=?;" andArguments:@[[NSNumber numberWithInt:MLOmemoInternalNotTrusted], self.accountID, jid]]; }]; } else { // untrust all of our own devices except our own device id [self.sqliteDatabase voidWriteTransaction:^{ [self.sqliteDatabase executeNonQuery:@"UPDATE signalContactIdentity SET trustLevel=? WHERE account_id=? AND contactName=? AND contactDeviceId!=?;" andArguments:@[[NSNumber numberWithInt:MLOmemoInternalNotTrusted], self.accountID, jid, [NSNumber numberWithUnsignedInt:self.deviceid]]]; }]; } } -(NSNumber*) getTrustLevel:(SignalAddress*) address identityKey:(NSData*) identityKey { return [self.sqliteDatabase idReadTransaction:^{ return [self.sqliteDatabase executeScalar:(@"SELECT \ CASE \ WHEN (trustLevel=0) THEN 0 \ WHEN (trustLevel=1 AND removedFromDeviceList IS NULL AND (lastReceivedMsg IS NULL OR lastReceivedMsg >= unixepoch('now', '-90 day'))) THEN 100 \ WHEN (trustLevel=1 AND removedFromDeviceList IS NOT NULL) THEN 101 \ WHEN (trustLevel=1 AND removedFromDeviceList IS NULL AND (lastReceivedMsg < unixepoch('now', '-90 day'))) THEN 102 \ WHEN (COUNT(*)=0) THEN 100 \ WHEN (trustLevel=2 AND removedFromDeviceList IS NULL AND (lastReceivedMsg IS NULL OR lastReceivedMsg >= unixepoch('now', '-90 day'))) THEN 200 \ WHEN (trustLevel=2 AND removedFromDeviceList IS NOT NULL) THEN 201 \ WHEN (trustLevel=2 AND removedFromDeviceList IS NULL AND (lastReceivedMsg < unixepoch('now', '-90 day'))) THEN 202 \ ELSE 0 \ END \ FROM signalContactIdentity \ WHERE account_id=? AND contactDeviceId=? AND contactName=? AND identity=?; \ ") andArguments:@[self.accountID, [NSNumber numberWithInteger:address.deviceId], address.name, identityKey]]; }]; } -(void) deleteDeviceforAddress:(SignalAddress*) address { [self.sqliteDatabase voidWriteTransaction:^{ [self.sqliteDatabase executeNonQuery:@"DELETE FROM signalContactIdentity WHERE account_id=? AND contactDeviceId=? AND contactName=?" andArguments:@[self.accountID, [NSNumber numberWithInteger:address.deviceId], address.name]]; }]; } // MUC session management // return true if we found at least one fingerprint for the given buddyJid -(BOOL) sessionsExistForBuddy:(NSString*) buddyJid { return [self.sqliteDatabase boolWriteTransaction:^{ NSNumber* contactDevicesExist = [self.sqliteDatabase executeScalar:@"SELECT COUNT(contactDeviceId) FROM signalContactIdentity WHERE account_id=? AND contactName=?;" andArguments:@[self.accountID, buddyJid]]; return (BOOL)(contactDevicesExist.intValue > 0); }]; } // delete the fingerprints and session for the given buddyJid if the jid is neither a 1:1 buddy nor a group member -(BOOL) checkIfSessionIsStillNeeded:(NSString*) buddyJid { return [self.sqliteDatabase boolWriteTransaction:^{ // delete fingerprints from buddyJid if the buddyJid is neither a buddy, a self chat, nor a group member NSNumber* buddyJidCnt = [self.sqliteDatabase executeScalar:@"SELECT \ (bCnt.buddyListCnt + mucCnt.roomCnt + accountCnt) \ FROM \ ( \ SELECT \ COUNT(buddy_name) AS buddyListCnt \ FROM buddylist \ WHERE \ account_id=? \ AND buddy_name=? \ AND Muc=0 \ ) AS bCnt, \ ( \ SELECT \ COUNT(m.room) AS roomCnt \ FROM muc_members AS m \ INNER JOIN buddylist AS b \ ON m.account_id = b.account_id \ AND b.buddy_name = m.member_jid \ WHERE \ b.account_id=? \ AND m.member_jid=? \ AND b.Muc=1 \ AND b.muc_type='group' \ ) AS mucCnt, \ ( \ SELECT \ COUNT(account_id) AS accountCnt \ FROM account \ WHERE \ (username || '@' || domain) = ? \ AND account_id = ? \ ) AS accountCnt" andArguments:@[self.accountID, buddyJid, self.accountID, buddyJid, self.accountID, buddyJid]]; BOOL buddyStillNeeded = buddyJidCnt.intValue > 0; if(buddyStillNeeded == NO) { [self.sqliteDatabase executeNonQuery:@"DELETE FROM signalContactIdentity \ WHERE \ account_id=? \ AND contactName=? \ " andArguments:@[self.accountID, buddyJid]]; [self.sqliteDatabase executeNonQuery:@"DELETE FROM signalContactSession \ WHERE \ account_id=? \ AND contactName=? \ " andArguments:@[self.accountID, buddyJid]]; } return buddyStillNeeded; }]; } // delete all fingerprints and sessions from contacts that are neither a buddy nor a group member // return jids of dangling sessions -(NSSet*) removeDanglingMucSessions { return [self.sqliteDatabase idWriteTransaction:^{ // create a list of all sessions NSArray* jidsWithSession = [self.sqliteDatabase executeScalarReader:@"SELECT DISTINCT contactName \ FROM signalContactIdentity \ WHERE \ account_id = ? \ " andArguments:@[self.accountID]]; NSMutableSet* danglingJids = [NSMutableSet new]; for(NSString* jid in jidsWithSession) { // check if the session is still needed if([self checkIfSessionIsStillNeeded:jid] == NO) { [danglingJids addObject:jid]; // delete old session [self.sqliteDatabase executeNonQuery:@"DELETE FROM signalContactIdentity \ WHERE \ account_id = ? \ AND contactName = ? \ " andArguments:@[self.accountID, jid]]; [self.sqliteDatabase executeNonQuery:@"DELETE FROM signalContactSession \ WHERE \ account_id = ? \ AND contactName = ? \ " andArguments:@[self.accountID, jid]]; } } return danglingJids; }]; } /** * Store a serialized sender key record for a given * (groupId + senderId + deviceId) tuple. */ -(BOOL) storeSenderKey:(nonnull NSData*) senderKey address:(nonnull SignalAddress*) address groupId:(nonnull NSString*) groupId; { return false; } /** * Returns a copy of the sender key record corresponding to the * (groupId + senderId + deviceId) tuple. */ - (nullable NSData*) loadSenderKeyForAddress:(SignalAddress*) address groupId:(NSString*) groupId; { return nil; } @end