diff --git a/CHANGELOG.md b/CHANGELOG.md index 1116814bc0..78d6b4c930 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * Self-assign the member role when joining with QSS [#3058](https://github.com/TryQuiet/quiet/issues/3058) * Use LFA-based identity in OrbitDB * Requests iOS notification permissions when app launches [#3079](https://github.com/TryQuiet/quiet/issues/3079) +* Store LFA keys in IOS keychain for notifications [#3091](https://github.com/TryQuiet/quiet/issues/3091) ### Fixes diff --git a/packages/backend/src/nest/auth/services/crypto/crypto.service.ts b/packages/backend/src/nest/auth/services/crypto/crypto.service.ts index 63af9863da..495aeb61ef 100644 --- a/packages/backend/src/nest/auth/services/crypto/crypto.service.ts +++ b/packages/backend/src/nest/auth/services/crypto/crypto.service.ts @@ -14,9 +14,10 @@ import { import { ChainServiceBase } from '../chainServiceBase' import { SigChain } from '../../sigchain' import { asymmetric, Keyset, Member, SignedEnvelope, EncryptStreamTeamPayload } from '@localfirst/auth' +import { KeyMap } from '@localfirst/auth/team/selectors' import { DEFAULT_SEARCH_OPTIONS, MemberSearchOptions } from '../members/types' import { createLogger } from '../../../common/logger' -import { KeyMetadata } from '3rd-party/auth/packages/crdx/dist' +import { KeyMetadata } from '@localfirst/crdx' const logger = createLogger('auth:cryptoService') @@ -36,6 +37,22 @@ class CryptoService extends ChainServiceBase { }) } + public getPublicKeysForAllMembers(includeSelf: boolean = false): Keyset[] { + const members = this.sigChain.users.getAllUsers() + const keysByMember = [] + for (const member of members) { + if (member.userId === this.sigChain.context.user.userId && !includeSelf) { + continue + } + keysByMember.push(member.keys) + } + return keysByMember + } + + public getAllKeys(): KeyMap { + return this.sigChain.team!.allKeys() + } + public sign(message: any): SignedEnvelope { return this.sigChain.team!.sign(message) } diff --git a/packages/backend/src/nest/auth/sigchain.service.ts b/packages/backend/src/nest/auth/sigchain.service.ts index dd417ad981..cdaacd35ee 100644 --- a/packages/backend/src/nest/auth/sigchain.service.ts +++ b/packages/backend/src/nest/auth/sigchain.service.ts @@ -1,21 +1,30 @@ -import { Inject, Injectable, OnModuleInit } from '@nestjs/common' +import { Inject, Injectable } from '@nestjs/common' import { SigChain } from './sigchain' -import { Connection, InviteeMemberContext, Keyring, LocalUserContext, MemberContext, Team } from '@localfirst/auth' +import { + Connection, + Hash, + InviteeMemberContext, + Keyring, + LocalUserContext, + MemberContext, + Team, + UserWithSecrets, + DeviceWithSecrets, +} from '@localfirst/auth' +import { KeyMetadata } from '@localfirst/crdx' import { LocalDbService } from '../local-db/local-db.service' import { createLogger } from '../common/logger' -import { SocketService } from '../socket/socket.service' -import { SocketEvents, User } from '@quiet/types' +import { SocketEvents, StorableKey, User } from '@quiet/types' import { type RoleService } from './services/roles/role.service' import { type DeviceService } from './services/members/device.service' import { type InviteService } from './services/invites/invite.service' import { type UserService } from './services/members/user.service' import { type CryptoService } from './services/crypto/crypto.service' -import { type UserWithSecrets } from '@localfirst/auth' -import { type DeviceWithSecrets } from '@localfirst/auth' import { SERVER_IO_PROVIDER } from '../const' import { ServerIoProviderTypes } from '../types' import EventEmitter from 'events' -import { GetChainFilter } from './types' +import { GetChainFilter, StoredKeyType } from './types' +import { KeysUpdatedEvent } from '@quiet/types' @Injectable() export class SigChainService extends EventEmitter { @@ -26,8 +35,7 @@ export class SigChainService extends EventEmitter { constructor( @Inject(SERVER_IO_PROVIDER) public readonly serverIoProvider: ServerIoProviderTypes, - private readonly localDbService: LocalDbService, - private readonly socketService: SocketService + private readonly localDbService: LocalDbService ) { super() } @@ -132,8 +140,19 @@ export class SigChainService extends EventEmitter { this.attachSocketListeners(this.getChain({ teamName })) } - private handleChainUpdate = () => { - const users = this.getActiveChain() + private handleChainUpdate = (teamName: string) => { + this._updateUsersOnChainUpdate(teamName) + this._updateKeysOnChainUpdate(teamName) + this.emit('updated', teamName) + this.saveChain(teamName) + this.logger.info('Chain updated, emitted updated event') + } + + /** + * Send updated list of users to the state manager on chain update + */ + private _updateUsersOnChainUpdate(teamName: string) { + const users = this.getChain({ teamName }) .team?.members() .map(user => ({ userId: user.userId, @@ -141,20 +160,101 @@ export class SigChainService extends EventEmitter { isRegistered: true, isDuplicated: false, })) as User[] - this.socketService.emit(SocketEvents.USERS_UPDATED, { users }) - this.emit('updated') - this.saveChain(this.activeChainTeamName!) - this.logger.info('Chain updated, emitted updated event') + this.serverIoProvider.io.emit(SocketEvents.USERS_UPDATED, { users }) + } + + /** + * Update the IOS keychain with any new keys on chain update + */ + private async _updateKeysOnChainUpdate(teamName: string): Promise { + if ((process.platform as string) !== 'ios') { + this.logger.trace('Skipping key update because we are not on ios, current platform =', process.platform) + return + } + + const generateKeyName = (teamId: string, keyType: string, scope: KeyMetadata): string => { + return `quiet_${teamId}_${scope.type}_${scope.name}_${scope.generation}_${keyType}` + } + + const sigchain = this.getChain({ teamName }) + if (sigchain == null) { + this.logger.error('No chain for name found', teamName) + return + } + + const teamId = sigchain.team!.id + const alreadySentKeys: Set = new Set(await this.localDbService.getKeysStoredInKeychain(teamId)) + const keysToSend: StorableKey[] = [] + const keyNamesSent: string[] = [] + // get all secret keys that this user has that haven't been added to the keychain + const allKeys = sigchain.crypto.getAllKeys() + for (const keyData of Object.values(allKeys)) { + for (const keyTypeData of Object.values(keyData)) { + for (const keyTypeGenData of Object.values(keyTypeData)) { + const keyName = generateKeyName(teamId, StoredKeyType.SECRET, { + name: keyTypeGenData.name, + type: keyTypeGenData.type, + generation: keyTypeGenData.generation, + }) + if (!alreadySentKeys.has(keyName)) { + keysToSend.push({ key: keyTypeGenData.secretKey, keyName }) + keyNamesSent.push(keyName) + } + } + } + } + // TODO: update to pull all generations of user public/sig keys + // get all user public keys that haven't been added to the keychain + const allUserPublicKeys = sigchain.crypto.getPublicKeysForAllMembers(true) + for (const keySet of allUserPublicKeys) { + const publicKeyName = generateKeyName(teamId, StoredKeyType.USER_PUBLIC, { + name: keySet.name, + type: keySet.type, + generation: keySet.generation, + }) + if (!alreadySentKeys.has(publicKeyName)) { + keysToSend.push({ key: keySet.encryption, keyName: publicKeyName }) + keyNamesSent.push(publicKeyName) + } + + const sigKeyName = generateKeyName(teamId, StoredKeyType.USER_SIG, { + name: keySet.name, + type: keySet.type, + generation: keySet.generation, + }) + if (!alreadySentKeys.has(sigKeyName)) { + keysToSend.push({ key: keySet.signature, keyName: sigKeyName }) + keyNamesSent.push(sigKeyName) + } + } + + if (keysToSend.length === 0) { + this.logger.trace('Skipping IOS keychain update, no new keys') + return + } + + // send new keys to the state manager to add to the keychain and update list of key names in + const keyUpdateEvent: KeysUpdatedEvent = { + keys: keysToSend, + } + await this.localDbService.updateKeysStoredInKeychain(teamId, keyNamesSent) + this.serverIoProvider.io.emit(SocketEvents.KEYS_UPDATED, keyUpdateEvent) } private attachSocketListeners(chain: SigChain): void { this.logger.info('Attaching socket listeners') - chain.on('updated', this.handleChainUpdate) + const _onTeamUpdate = (): void => { + this.handleChainUpdate(chain.team!.teamName) + } + chain.on('updated', _onTeamUpdate) } private detachSocketListeners(chain: SigChain): void { this.logger.info('Detaching socket listeners') - chain.removeListener('updated', this.handleChainUpdate) + const _onTeamUpdate = (): void => { + this.handleChainUpdate(chain.team!.teamName) + } + chain.removeListener('updated', _onTeamUpdate) } /** @@ -208,6 +308,7 @@ export class SigChainService extends EventEmitter { const sigChain = SigChain.create(teamName, username) this.addChain(sigChain, setActive, teamName) await this.saveChain(teamName) + this.handleChainUpdate(teamName) return sigChain } diff --git a/packages/backend/src/nest/auth/types.ts b/packages/backend/src/nest/auth/types.ts index 438c6b0d46..42d7eda60a 100644 --- a/packages/backend/src/nest/auth/types.ts +++ b/packages/backend/src/nest/auth/types.ts @@ -17,3 +17,9 @@ export type GetChainFilter = { teamId?: string teamName?: string } + +export enum StoredKeyType { + SECRET = 'secret', + USER_PUBLIC = 'userPublic', + USER_SIG = 'userSig', +} diff --git a/packages/backend/src/nest/local-db/local-db.service.ts b/packages/backend/src/nest/local-db/local-db.service.ts index b1cd84f38d..d1209bcb12 100644 --- a/packages/backend/src/nest/local-db/local-db.service.ts +++ b/packages/backend/src/nest/local-db/local-db.service.ts @@ -605,4 +605,29 @@ export class LocalDbService extends EventEmitter { } return count } + + /** + * Update list of kys for a given team ID that were stored in the IOS keychain + * + * @param teamId LFA team ID + * @param keyNames Names of keys that were added to IOS keychain + */ + public async updateKeysStoredInKeychain(teamId: string, keyNames: string[]): Promise { + const key = `${LocalDBKeys.KEYS_STORED_KEYCHAIN}:${teamId}` + const arr: string[] = (await this.get(key)) || [] + arr.push(...keyNames) + await this.put(key, arr) + } + + /** + * Get the list of key names for a given team ID that have been stored in the IOS keychain + * + * @param teamId LFA team ID + * @returns List of key names + */ + public async getKeysStoredInKeychain(teamId: string): Promise { + const key = `${LocalDBKeys.KEYS_STORED_KEYCHAIN}:${teamId}` + const arr: string[] = (await this.get(key)) || [] + return arr + } } diff --git a/packages/backend/src/nest/local-db/local-db.types.ts b/packages/backend/src/nest/local-db/local-db.types.ts index e0ccf2236b..1ddf242ae5 100644 --- a/packages/backend/src/nest/local-db/local-db.types.ts +++ b/packages/backend/src/nest/local-db/local-db.types.ts @@ -38,6 +38,9 @@ export enum LocalDBKeys { // exists in the Community object. OWNER_ORBIT_DB_IDENTITY = 'ownerOrbitDbIdentity', + // Keys from sigchain that have been stored in keychain + KEYS_STORED_KEYCHAIN = 'keysStoredInKeychain', + SIGCHAINS = 'sigchains:', USER_CONTEXTS = 'userContexts', KEYRINGS = 'keyrings', diff --git a/packages/backend/src/nest/qss/qss.service.spec.ts b/packages/backend/src/nest/qss/qss.service.spec.ts index 7b9e2e451e..31e5dff651 100644 --- a/packages/backend/src/nest/qss/qss.service.spec.ts +++ b/packages/backend/src/nest/qss/qss.service.spec.ts @@ -1035,7 +1035,7 @@ describe('QSSService', () => { const ingestSpy = jest.spyOn(orbitDbService, 'ingestEntries').mockResolvedValue() // Trigger sigchain update which should process DLQ - sigchainService.emit('updated') + sigchainService.emit('updated', sigchainService.activeChainTeamName) // Wait for async processing await waitForExpect(async () => { @@ -1071,10 +1071,10 @@ describe('QSSService', () => { const processSpy = jest.spyOn(qssService, 'processDLQDecrypt') // Trigger first update - sigchainService.emit('updated') + sigchainService.emit('updated', sigchainService.activeChainTeamName) // Immediately trigger second update while first is processing - sigchainService.emit('updated') + sigchainService.emit('updated', sigchainService.activeChainTeamName) await waitForExpect(async () => { const remainingCount = await localDbService.getDLQDecryptCount(teamId) @@ -1097,7 +1097,7 @@ describe('QSSService', () => { const ingestSpy = jest.spyOn(orbitDbService, 'ingestEntries') // Trigger sigchain update - sigchainService.emit('updated') + sigchainService.emit('updated', sigchainService.activeChainTeamName) // Give it time to process await new Promise(resolve => setTimeout(resolve, 100)) diff --git a/packages/backend/src/nest/qss/qss.service.ts b/packages/backend/src/nest/qss/qss.service.ts index 9a06a8b120..0a67374320 100644 --- a/packages/backend/src/nest/qss/qss.service.ts +++ b/packages/backend/src/nest/qss/qss.service.ts @@ -105,7 +105,7 @@ export class QSSService extends EventEmitter implements OnModuleDestroy, OnModul this._deadLetterQueueProcessor = setInterval(this.processDeadLetterQueue, 30_000) this.connect = this.connect.bind(this) this._configureEventHandlers() - this.sigChainService.on('updated', () => void this.processDLQDecrypt()) + this.sigChainService.on('updated', (teamName: string) => void this.processDLQDecrypt(teamName)) } public onModuleDestroy() { @@ -907,14 +907,14 @@ export class QSSService extends EventEmitter implements OnModuleDestroy, OnModul /** * Process the decryption dead letter queue when sigchain updates (new keys arrive) */ - private async processDLQDecrypt(): Promise { + private async processDLQDecrypt(teamName: string): Promise { if (this._dlqDecryptInFlight) { this.logger.debug('DLQ decrypt already in progress, requesting retry') this._dlqDecryptRetryRequested = true return } - const activeChain = this.sigChainService.getActiveChain(false) + const activeChain = this.sigChainService.getChain({ teamName }) if (!activeChain?.team) { return } @@ -981,7 +981,7 @@ export class QSSService extends EventEmitter implements OnModuleDestroy, OnModul // If a sigchain update occurred while processing, retry with new keys if (this._dlqDecryptRetryRequested) { this.logger.debug('Retrying DLQ decrypt after sigchain update during processing') - await this.processDLQDecrypt() + await this.processDLQDecrypt(teamName) } } diff --git a/packages/mobile/ios/CommunicationBridge.m b/packages/mobile/ios/CommunicationBridge.m index 254cfa1975..5f0e6155ef 100644 --- a/packages/mobile/ios/CommunicationBridge.m +++ b/packages/mobile/ios/CommunicationBridge.m @@ -5,4 +5,5 @@ @interface RCT_EXTERN_MODULE(CommunicationModule, RCTEventEmitter) RCT_EXTERN_METHOD(handleIncomingEvents:(NSString *)event payload:(NSString *)payload extra:(NSString *)extra) RCT_EXTERN_METHOD(requestNotificationPermission) RCT_EXTERN_METHOD(checkNotificationPermission) +RCT_EXTERN_METHOD(saveKeysInKeychain:(NSArray *)newKeys) @end diff --git a/packages/mobile/ios/CommunicationModule.swift b/packages/mobile/ios/CommunicationModule.swift index f6961cdbfe..09c7b62330 100644 --- a/packages/mobile/ios/CommunicationModule.swift +++ b/packages/mobile/ios/CommunicationModule.swift @@ -1,4 +1,5 @@ import UserNotifications +import OSLog @objc(CommunicationModule) class CommunicationModule: RCTEventEmitter { @@ -12,6 +13,9 @@ class CommunicationModule: RCTEventEmitter { static let DEVICE_TOKEN_RECEIVED = "deviceTokenReceived" static let WEBSOCKET_CONNECTION_CHANNEL = "_WEBSOCKET_CONNECTION_" + private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "CommunicationModule") + + let keychainHandler = KeychainHandler() @objc func sendDataPort(port: UInt16, socketIOSecret: String) { @@ -55,6 +59,24 @@ class CommunicationModule: RCTEventEmitter { } } } + + @objc + func saveKeysInKeychain(_ newKeys: NSArray) { + let decoder = JSONDecoder() + for keyAsAny in newKeys { + do { + let keyAsString: String = keyAsAny as! String + let data = Data(keyAsString.utf8) + let decodedNamedKey = try decoder.decode(NamedKey.self, from: data) + try self.keychainHandler.addLfaKey(namedKey: decodedNamedKey) + let stored = try self.keychainHandler.getLfaKeyString(keyName: decodedNamedKey.keyName) + CommunicationModule.logger.info("Stored key matches? \(stored == decodedNamedKey.key) \(decodedNamedKey.keyName)") + } catch { + // TODO: send a message to the backend with any keys that weren't stored + CommunicationModule.logger.error("Error while saving key in keychain: \(error)") + } + } + } @objc func checkNotificationPermission() { diff --git a/packages/mobile/ios/KeychainHandler.swift b/packages/mobile/ios/KeychainHandler.swift new file mode 100644 index 0000000000..d478d84948 --- /dev/null +++ b/packages/mobile/ios/KeychainHandler.swift @@ -0,0 +1,136 @@ +// +// KeychainError.swift +// Quiet +// +// Created by Isla Koenigsknecht on 2/25/26. +// + + +import CryptoKit +import Security +import CoreData +import OSLog + +public enum KeychainError: Error { + case noPassword + case unexpectedPasswordData + case unexpectedItemData + case unhandledError(status: OSStatus) +} + +public enum ConversionError: Error { + case stringToBytesError +} + +public enum KeychainHandlerError: Error { + case noKeyFound + case malformedKey + case unhandledError(reason: Any) +} + +public enum KeyAddStatus { + case success + case duplicateScope +} + +public struct NamedKey: Codable { + let keyName: String + let key: String +} + +// TODO: add string to key object conversion (e.g. string to SymmetricKey) +@objc(KeychainHandler) +class KeychainHandler: NSObject { + private let keychainGroupName: String = "com.quietmobile" + + private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "KeychainHandler") + + public func getLfaKeyString(keyName: String) throws -> String { + do { + let password: String = try _getKeyImpl(keyName: keyName) + return password + } catch KeychainError.noPassword { + throw KeychainHandlerError.noKeyFound + } catch KeychainError.unexpectedPasswordData { + throw KeychainHandlerError.malformedKey + } catch ConversionError.stringToBytesError { + throw KeychainHandlerError.malformedKey + } catch { + throw KeychainHandlerError.unhandledError(reason: error) + } + } + + public func addLfaKey(namedKey: NamedKey) throws -> KeyAddStatus { + var existingKey: String? + do { + existingKey = try getLfaKeyString(keyName: namedKey.keyName) + } catch KeychainHandlerError.noKeyFound { + existingKey = nil + } catch KeychainHandlerError.malformedKey { + existingKey = nil + } catch { + KeychainHandler.logger.error("Error while getting existing LFA key for name \(namedKey.keyName): \(error)") + throw error + } + + guard existingKey == nil else { + guard existingKey == namedKey.key else { return KeyAddStatus.duplicateScope } + return KeyAddStatus.success + } + + do { + let keyData: Data = try _stringToBytes(str: namedKey.key) + let addStatus: KeyAddStatus = try _addKeyToKeychainImpl(keyName: namedKey.keyName, keyData: keyData) + return addStatus + } catch { + throw KeychainHandlerError.unhandledError(reason: error) + } + } + + private func _getKeyImpl(keyName: String) throws -> String { + var existingKey: CFTypeRef? + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: keychainGroupName, + kSecAttrAccount as String: keyName, + kSecMatchLimit as String: kSecMatchLimitOne, + kSecReturnAttributes as String: true, + kSecReturnData as String: true + ] + let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &existingKey) + guard status != errSecItemNotFound else { throw KeychainError.noPassword } + guard status == errSecSuccess else { throw KeychainError.unhandledError(status: status) } + guard let existingItem: [String : Any] = existingKey as? [String : Any], + let passwordData = existingItem[kSecValueData as String] as? Data, + let password = String(data: passwordData, encoding: String.Encoding.utf8) + else { + throw KeychainError.unexpectedPasswordData + } + return password + } + + private func _addKeyToKeychainImpl(keyName: String, keyData: Data) throws -> KeyAddStatus { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: keyName, + kSecAttrService as String: keychainGroupName, + kSecValueData as String: keyData, + kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock + ] + + let status: OSStatus = SecItemAdd(query as CFDictionary, nil) + if status == errSecSuccess { + return KeyAddStatus.success + } else if status == errSecDuplicateItem { + return KeyAddStatus.duplicateScope + } else { + throw KeychainError.unhandledError(status: status) + } + } + + private func _stringToBytes(str: String) throws -> Data { + let bytes: Data? = str.data(using: .utf8) + guard bytes != nil else { throw ConversionError.stringToBytesError } + return bytes! + } +} diff --git a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj index 79efc9d819..e1d226dc60 100644 --- a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj @@ -55,10 +55,11 @@ 18FD2A3E296F009E00A2B8C0 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 18FD2A37296F009E00A2B8C0 /* AppDelegate.m */; }; 18FD2A3F296F009E00A2B8C0 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 18FD2A38296F009E00A2B8C0 /* Images.xcassets */; }; 18FD2A40296F009E00A2B8C0 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 18FD2A39296F009E00A2B8C0 /* main.m */; }; - 38AD376324628C6E27E70991 /* libPods-Quiet-QuietTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 02AFA323BA7196A67E7A1133 /* libPods-Quiet-QuietTests.a */; }; + 665587CA2F4F5ECD005D2086 /* KeychainHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665587C92F4F5ECD005D2086 /* KeychainHandler.swift */; }; + 85137B01156B661E82184B11 /* libPods-Quiet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 287A03B12772D68AAF979A77 /* libPods-Quiet.a */; }; 955DC7582BD930B30014725B /* WebsocketSingleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955DC7572BD930B30014725B /* WebsocketSingleton.swift */; }; D3239FB5EFA85E780E1AD201 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 84F12DFE2A5B0E05C2C41286 /* PrivacyInfo.xcprivacy */; }; - E479975299F0932FF690F805 /* libPods-Quiet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 104C0D0FA8EF00192C60CAD7 /* libPods-Quiet.a */; }; + FFD33AE2521CEA2E0D3134ED /* libPods-Quiet-QuietTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CA54D67D8C7A248C6A22C0F8 /* libPods-Quiet-QuietTests.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -92,7 +93,6 @@ 00E356EE1AD99517003FC87E /* QuietTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = QuietTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* QuietTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QuietTests.m; sourceTree = ""; }; - 02AFA323BA7196A67E7A1133 /* libPods-Quiet-QuietTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Quiet-QuietTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 03B673F92E6103DC00A86655 /* Rubik-Black.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Rubik-Black.ttf"; path = "../src/assets/fonts/Rubik-Black.ttf"; sourceTree = SOURCE_ROOT; }; 03B673FA2E6103DC00A86655 /* Rubik-BlackItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Rubik-BlackItalic.ttf"; path = "../src/assets/fonts/Rubik-BlackItalic.ttf"; sourceTree = SOURCE_ROOT; }; 03B673FB2E6103DC00A86655 /* Rubik-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Rubik-Bold.ttf"; path = "../src/assets/fonts/Rubik-Bold.ttf"; sourceTree = SOURCE_ROOT; }; @@ -107,8 +107,6 @@ 03B674042E6103DC00A86655 /* Rubik-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Rubik-Regular.ttf"; path = "../src/assets/fonts/Rubik-Regular.ttf"; sourceTree = SOURCE_ROOT; }; 03B674052E6103DC00A86655 /* Rubik-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Rubik-SemiBold.ttf"; path = "../src/assets/fonts/Rubik-SemiBold.ttf"; sourceTree = SOURCE_ROOT; }; 03B674062E6103DC00A86655 /* Rubik-SemiBoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Rubik-SemiBoldItalic.ttf"; path = "../src/assets/fonts/Rubik-SemiBoldItalic.ttf"; sourceTree = SOURCE_ROOT; }; - 104C0D0FA8EF00192C60CAD7 /* libPods-Quiet.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Quiet.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 116749B7D5552021F04BE739 /* Pods-Quiet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet.release.xcconfig"; path = "Target Support Files/Pods-Quiet/Pods-Quiet.release.xcconfig"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* Quiet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Quiet.app; sourceTree = BUILT_PRODUCTS_DIR; }; 180E120A2AEFB7F900804659 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; 1827A9E129783D6E00245FD3 /* classic-level.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = "classic-level.framework"; sourceTree = ""; }; @@ -620,11 +618,15 @@ 18FD2A39296F009E00A2B8C0 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Quiet/main.m; sourceTree = ""; }; 18FD2A3A296F009E00A2B8C0 /* Quiet.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; name = Quiet.entitlements; path = Quiet/Quiet.entitlements; sourceTree = ""; }; 18FD2A3B296F009E00A2B8C0 /* QuietDebug.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; name = QuietDebug.entitlements; path = Quiet/QuietDebug.entitlements; sourceTree = ""; }; - 2877B6088D1D81EF869D71D9 /* Pods-Quiet-QuietTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet-QuietTests.release.xcconfig"; path = "Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests.release.xcconfig"; sourceTree = ""; }; - 7F75CE6D1C7D3DA9F5BA6A76 /* Pods-Quiet-QuietTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet-QuietTests.debug.xcconfig"; path = "Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests.debug.xcconfig"; sourceTree = ""; }; + 287A03B12772D68AAF979A77 /* libPods-Quiet.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Quiet.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2F63F73503B4A62481768267 /* Pods-Quiet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet.debug.xcconfig"; path = "Target Support Files/Pods-Quiet/Pods-Quiet.debug.xcconfig"; sourceTree = ""; }; + 665587C92F4F5ECD005D2086 /* KeychainHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHandler.swift; sourceTree = ""; }; + 677CCEF15F36760C73BC26A6 /* Pods-Quiet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet.release.xcconfig"; path = "Target Support Files/Pods-Quiet/Pods-Quiet.release.xcconfig"; sourceTree = ""; }; + 70BDAC54548DB3505251D3DB /* Pods-Quiet-QuietTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet-QuietTests.debug.xcconfig"; path = "Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests.debug.xcconfig"; sourceTree = ""; }; 84F12DFE2A5B0E05C2C41286 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Quiet/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 88972EF0A58F6401DC66730D /* Pods-Quiet-QuietTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet-QuietTests.release.xcconfig"; path = "Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests.release.xcconfig"; sourceTree = ""; }; 955DC7572BD930B30014725B /* WebsocketSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsocketSingleton.swift; sourceTree = ""; }; - E727FE0446E9E46CDECEC7FE /* Pods-Quiet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet.debug.xcconfig"; path = "Target Support Files/Pods-Quiet/Pods-Quiet.debug.xcconfig"; sourceTree = ""; }; + CA54D67D8C7A248C6A22C0F8 /* libPods-Quiet-QuietTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Quiet-QuietTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -633,7 +635,7 @@ buildActionMask = 2147483647; files = ( 1827A9E229783D6E00245FD3 /* classic-level.framework in Frameworks */, - 38AD376324628C6E27E70991 /* libPods-Quiet-QuietTests.a in Frameworks */, + FFD33AE2521CEA2E0D3134ED /* libPods-Quiet-QuietTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -643,7 +645,7 @@ files = ( 00A416342EC2EAA900ACC877 /* NodeMobile.xcframework in Frameworks */, 1827A9E329783D7600245FD3 /* classic-level.framework in Frameworks */, - E479975299F0932FF690F805 /* libPods-Quiet.a in Frameworks */, + 85137B01156B661E82184B11 /* libPods-Quiet.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -670,6 +672,7 @@ 13B07FAE1A68108700A75B9A /* Quiet */ = { isa = PBXGroup; children = ( + 665587C92F4F5ECD005D2086 /* KeychainHandler.swift */, 180E120A2AEFB7F900804659 /* Utils.swift */, 18FD2A36296F009E00A2B8C0 /* AppDelegate.h */, 18FD2A37296F009E00A2B8C0 /* AppDelegate.m */, @@ -4701,10 +4704,10 @@ 1CEEDB4F07B9978C125775C5 /* Pods */ = { isa = PBXGroup; children = ( - E727FE0446E9E46CDECEC7FE /* Pods-Quiet.debug.xcconfig */, - 116749B7D5552021F04BE739 /* Pods-Quiet.release.xcconfig */, - 7F75CE6D1C7D3DA9F5BA6A76 /* Pods-Quiet-QuietTests.debug.xcconfig */, - 2877B6088D1D81EF869D71D9 /* Pods-Quiet-QuietTests.release.xcconfig */, + 2F63F73503B4A62481768267 /* Pods-Quiet.debug.xcconfig */, + 677CCEF15F36760C73BC26A6 /* Pods-Quiet.release.xcconfig */, + 70BDAC54548DB3505251D3DB /* Pods-Quiet-QuietTests.debug.xcconfig */, + 88972EF0A58F6401DC66730D /* Pods-Quiet-QuietTests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -4714,8 +4717,8 @@ children = ( 00A416332EC2EAA900ACC877 /* NodeMobile.xcframework */, 1827A9E129783D6E00245FD3 /* classic-level.framework */, - 104C0D0FA8EF00192C60CAD7 /* libPods-Quiet.a */, - 02AFA323BA7196A67E7A1133 /* libPods-Quiet-QuietTests.a */, + 287A03B12772D68AAF979A77 /* libPods-Quiet.a */, + CA54D67D8C7A248C6A22C0F8 /* libPods-Quiet-QuietTests.a */, ); name = Frameworks; sourceTree = ""; @@ -4781,12 +4784,12 @@ isa = PBXNativeTarget; buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "QuietTests" */; buildPhases = ( - 58352AE7BD90BC0845FC78DE /* [CP] Check Pods Manifest.lock */, + 95F7BA6D16EB3A1B27BAA786 /* [CP] Check Pods Manifest.lock */, 00E356EA1AD99517003FC87E /* Sources */, 00E356EB1AD99517003FC87E /* Frameworks */, 00E356EC1AD99517003FC87E /* Resources */, - EF4E9879B3A5060ADF995D6A /* [CP] Embed Pods Frameworks */, - 4E18C6AEAD8D524EAD836F51 /* [CP] Copy Pods Resources */, + 911E369A1C6D85907F7F0302 /* [CP] Embed Pods Frameworks */, + E7F2776DE3547BE1502BC947 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -4802,7 +4805,7 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Quiet" */; buildPhases = ( - 715B735C8E7FA602A967EAF0 /* [CP] Check Pods Manifest.lock */, + BB9294C6673D9CCB22CBA0B8 /* [CP] Check Pods Manifest.lock */, FD10A7F022414F080027D42C /* Start Packager */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, @@ -4814,8 +4817,8 @@ 18FD2A32296D736300A2B8C0 /* [CUSTOM NODEJS MOBILE] Remove Python3 Binaries */, 1827A9E0297837FE00245FD3 /* [CUSTOM NODEJS MOBILE] Remove prebuilds */, 1868C095292F8FE2001D6D5E /* Embed Frameworks */, - 7253A52F091FC8122D730B2E /* [CP] Embed Pods Frameworks */, - A1AE2657AE5C40D18941D671 /* [CP] Copy Pods Resources */, + 85BBD2CD345055BC7227A72B /* [CP] Embed Pods Frameworks */, + 00462B5D3156F56526040199 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -4912,6 +4915,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 00462B5D3156F56526040199 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -4924,7 +4944,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -e\n\n# Fix for machines using nvm\nif [[ -s \"$HOME/.nvm/nvm.sh\" ]]; then\n. \"$HOME/.nvm/nvm.sh\"\nelif [[ -x \"$(command -v brew)\" && -s \"$(brew --prefix nvm)/nvm.sh\" ]]; then\n. \"$(brew --prefix nvm)/nvm.sh\"\nfi\n\nexport NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh\n"; + shellScript = "set -e\n\nexport NODE_BINARY=/Users/isla/.volta/bin/node\n../node_modules/react-native/scripts/react-native-xcode.sh\n"; }; 03B673F82E60FE0000A86655 /* Inject Feature Flags */ = { isa = PBXShellScriptBuildPhase; @@ -5020,46 +5040,41 @@ shellPath = /bin/sh; shellScript = "find \"$CODESIGNING_FOLDER_PATH/nodejs-project/node_modules/\" -name \"python3\" | xargs rm\n"; }; - 4E18C6AEAD8D524EAD836F51 /* [CP] Copy Pods Resources */ = { + 85BBD2CD345055BC7227A72B /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-resources-${CONFIGURATION}-input-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Copy Pods Resources"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-resources-${CONFIGURATION}-output-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 58352AE7BD90BC0845FC78DE /* [CP] Check Pods Manifest.lock */ = { + 911E369A1C6D85907F7F0302 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Quiet-QuietTests-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 715B735C8E7FA602A967EAF0 /* [CP] Check Pods Manifest.lock */ = { + 95F7BA6D16EB3A1B27BAA786 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -5074,62 +5089,50 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Quiet-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Quiet-QuietTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 7253A52F091FC8122D730B2E /* [CP] Embed Pods Frameworks */ = { + BB9294C6673D9CCB22CBA0B8 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - A1AE2657AE5C40D18941D671 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-resources-${CONFIGURATION}-input-files.xcfilelist", + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "[CP] Copy Pods Resources"; + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Quiet-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-resources.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - EF4E9879B3A5060ADF995D6A /* [CP] Embed Pods Frameworks */ = { + E7F2776DE3547BE1502BC947 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-resources-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + name = "[CP] Copy Pods Resources"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-resources.sh\"\n"; showEnvVarsInLog = 0; }; FD10A7F022414F080027D42C /* Start Packager */ = { @@ -5174,6 +5177,7 @@ 1868BCEE292E9212001D6D5E /* RNNodeJsMobile.m in Sources */, 1868C4382930D7D6001D6D5E /* DataDirectory.swift in Sources */, 1868C43E2930EAEA001D6D5E /* CommunicationBridge.m in Sources */, + 665587CA2F4F5ECD005D2086 /* KeychainHandler.swift in Sources */, 1868C43A2930D859001D6D5E /* FindFreePort.swift in Sources */, 18FD2A3E296F009E00A2B8C0 /* AppDelegate.m in Sources */, 1868BCED292E9212001D6D5E /* NodeRunner.mm in Sources */, @@ -5195,7 +5199,7 @@ /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7F75CE6D1C7D3DA9F5BA6A76 /* Pods-Quiet-QuietTests.debug.xcconfig */; + baseConfigurationReference = 70BDAC54548DB3505251D3DB /* Pods-Quiet-QuietTests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -5228,7 +5232,7 @@ }; 00E356F71AD99517003FC87E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 2877B6088D1D81EF869D71D9 /* Pods-Quiet-QuietTests.release.xcconfig */; + baseConfigurationReference = 88972EF0A58F6401DC66730D /* Pods-Quiet-QuietTests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -5258,7 +5262,7 @@ }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E727FE0446E9E46CDECEC7FE /* Pods-Quiet.debug.xcconfig */; + baseConfigurationReference = 2F63F73503B4A62481768267 /* Pods-Quiet.debug.xcconfig */; buildSettings = { ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -5359,7 +5363,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 116749B7D5552021F04BE739 /* Pods-Quiet.release.xcconfig */; + baseConfigurationReference = 677CCEF15F36760C73BC26A6 /* Pods-Quiet.release.xcconfig */; buildSettings = { ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; diff --git a/packages/mobile/ios/Quiet/Quiet.entitlements b/packages/mobile/ios/Quiet/Quiet.entitlements index 903def2af5..599333ab05 100644 --- a/packages/mobile/ios/Quiet/Quiet.entitlements +++ b/packages/mobile/ios/Quiet/Quiet.entitlements @@ -4,5 +4,9 @@ aps-environment development + keychain-access-groups + + $(AppIdentifierPrefix)com.quietmobile + diff --git a/packages/mobile/ios/Quiet/QuietDebug.entitlements b/packages/mobile/ios/Quiet/QuietDebug.entitlements index 903def2af5..599333ab05 100644 --- a/packages/mobile/ios/Quiet/QuietDebug.entitlements +++ b/packages/mobile/ios/Quiet/QuietDebug.entitlements @@ -4,5 +4,9 @@ aps-environment development + keychain-access-groups + + $(AppIdentifierPrefix)com.quietmobile + diff --git a/packages/mobile/src/store/init/startConnection/startConnection.saga.ts b/packages/mobile/src/store/init/startConnection/startConnection.saga.ts index 10c9b3370f..6a83e68746 100644 --- a/packages/mobile/src/store/init/startConnection/startConnection.saga.ts +++ b/packages/mobile/src/store/init/startConnection/startConnection.saga.ts @@ -16,9 +16,10 @@ import { PayloadAction } from '@reduxjs/toolkit' import { socket as stateManager, Socket } from '@quiet/state-manager' import { initActions, WebsocketConnectionPayload } from '../init.slice' import { eventChannel } from 'redux-saga' -import { SocketActions } from '@quiet/types' +import { KeysUpdatedEvent, SocketActions, SocketEvents } from '@quiet/types' import { createLogger } from '../../../utils/logger' import { initSelectors } from '../init.selectors' +import { keysActions } from '../../keys/keys.slice' const logger = createLogger('startConnection') @@ -76,7 +77,9 @@ function subscribeSocketLifecycle(socket: Socket, socketIOData: WebsocketConnect let socket_id: string | undefined return eventChannel< - ReturnType | ReturnType + | ReturnType + | ReturnType + | ReturnType >(emit => { socket.on('connect', async () => { socket_id = socket.id @@ -87,6 +90,10 @@ function subscribeSocketLifecycle(socket: Socket, socketIOData: WebsocketConnect logger.warn('client: Closing socket connection', socket_id, reason) emit(initActions.suspendWebsocketConnection()) }) + socket.on(SocketEvents.KEYS_UPDATED, async (payload: KeysUpdatedEvent) => { + logger.info('Keys updated, writing to keychain') + emit(keysActions.saveKeysInKeychain(payload)) + }) return () => {} }) } diff --git a/packages/mobile/src/store/keys/keys.master.saga.ts b/packages/mobile/src/store/keys/keys.master.saga.ts new file mode 100644 index 0000000000..a35dff4a2a --- /dev/null +++ b/packages/mobile/src/store/keys/keys.master.saga.ts @@ -0,0 +1,20 @@ +import { takeEvery, cancelled } from 'redux-saga/effects' +import { all } from 'typed-redux-saga' +import { type Socket } from '@quiet/state-manager/src/types' +import { keysActions } from './keys.slice' +import { saveKeysInKeychainSaga } from './saveKeysInKeychain/saveKeysInKeychain.saga' +import { createLogger } from '../../utils/logger' + +const logger = createLogger('keysMasterSaga') + +export function* keysMasterSaga(): Generator { + logger.info('keysMasterSaga starting') + try { + yield all([takeEvery(keysActions.saveKeysInKeychain.type, saveKeysInKeychainSaga)]) + } finally { + logger.info('keysMasterSaga stopping') + if (yield cancelled()) { + logger.info('keysMasterSaga cancelled') + } + } +} diff --git a/packages/mobile/src/store/keys/keys.selectors..ts b/packages/mobile/src/store/keys/keys.selectors..ts new file mode 100644 index 0000000000..647e429649 --- /dev/null +++ b/packages/mobile/src/store/keys/keys.selectors..ts @@ -0,0 +1,6 @@ +import { StoreKeys } from '../store.keys' +import { CreatedSelectors, StoreState } from '../store.types' + +const keysSlice: CreatedSelectors[StoreKeys.Keys] = (state: StoreState) => state[StoreKeys.Keys] + +export const keysSelectors = {} diff --git a/packages/mobile/src/store/keys/keys.slice.ts b/packages/mobile/src/store/keys/keys.slice.ts new file mode 100644 index 0000000000..884b147bcf --- /dev/null +++ b/packages/mobile/src/store/keys/keys.slice.ts @@ -0,0 +1,19 @@ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit' +import { StoreKeys } from '../store.keys' +import { KeysUpdatedEvent } from '@quiet/types' +import { createLogger } from '../../utils/logger' + +const logger = createLogger('keysSlice') + +export class KeysState {} + +export const keysSlice = createSlice({ + initialState: { ...new KeysState() }, + name: StoreKeys.Keys, + reducers: { + saveKeysInKeychain: (state, _action: PayloadAction) => state, + }, +}) + +export const keysActions = keysSlice.actions +export const keysReducer = keysSlice.reducer diff --git a/packages/mobile/src/store/keys/keys.transform.ts b/packages/mobile/src/store/keys/keys.transform.ts new file mode 100644 index 0000000000..797b644871 --- /dev/null +++ b/packages/mobile/src/store/keys/keys.transform.ts @@ -0,0 +1,14 @@ +import { createTransform } from 'redux-persist' +import { StoreKeys } from '../store.keys' +import { KeysState } from './keys.slice' + +export const KeysTransform = createTransform( + (inboundState: KeysState, _key: any) => { + return inboundState + }, + (outboundState: KeysState, _key: any) => { + // TODO: determine if we still need this transform + return outboundState + }, + { whitelist: [StoreKeys.Keys] } +) diff --git a/packages/mobile/src/store/keys/keys.type.ts b/packages/mobile/src/store/keys/keys.type.ts new file mode 100644 index 0000000000..5148b443a9 --- /dev/null +++ b/packages/mobile/src/store/keys/keys.type.ts @@ -0,0 +1,12 @@ +export type ExtendedKeyScope = { + type: string + name: string + generation: number + keyType: string +} + +export interface StorableKey { + scope: ExtendedKeyScope + key: string + teamId: string +} diff --git a/packages/mobile/src/store/keys/saveKeysInKeychain/saveKeysInKeychain.saga.ts b/packages/mobile/src/store/keys/saveKeysInKeychain/saveKeysInKeychain.saga.ts new file mode 100644 index 0000000000..45c6465713 --- /dev/null +++ b/packages/mobile/src/store/keys/saveKeysInKeychain/saveKeysInKeychain.saga.ts @@ -0,0 +1,20 @@ +import { type PayloadAction } from '@reduxjs/toolkit' +import { call } from 'typed-redux-saga' +import { NativeModules } from 'react-native' + +import { KeysUpdatedEvent } from '@quiet/types' +import { createLogger } from '../../../utils/logger' + +const logger = createLogger('saveKeysInKeychainSaga') + +export function* saveKeysInKeychainSaga(action: PayloadAction): Generator { + logger.info('Storing keys in ios keychain', action.payload.keys) + try { + yield* call( + NativeModules.CommunicationModule.saveKeysInKeychain, + action.payload.keys.map(key => JSON.stringify(key)) + ) + } catch (e) { + logger.error('Error while updating keys on keychain', e) + } +} diff --git a/packages/mobile/src/store/root.reducer.ts b/packages/mobile/src/store/root.reducer.ts index f5570fc579..0ffbbbb304 100644 --- a/packages/mobile/src/store/root.reducer.ts +++ b/packages/mobile/src/store/root.reducer.ts @@ -5,6 +5,7 @@ import { initReducer } from './init/init.slice' import { navigationReducer } from './navigation/navigation.slice' import { nativeServicesReducer, nativeServicesActions } from './nativeServices/nativeServices.slice' import { pushNotificationsReducer } from './pushNotifications/pushNotifications.slice' +import { keysReducer } from './keys/keys.slice' export const reducers = { ...stateManagerReducers.reducers, @@ -12,6 +13,7 @@ export const reducers = { [StoreKeys.Navigation]: navigationReducer, [StoreKeys.NativeServices]: nativeServicesReducer, [StoreKeys.PushNotifications]: pushNotificationsReducer, + [StoreKeys.Keys]: keysReducer, } export const allReducers = combineReducers(reducers) diff --git a/packages/mobile/src/store/root.saga.ts b/packages/mobile/src/store/root.saga.ts index c17191aa3d..98a149f6a6 100644 --- a/packages/mobile/src/store/root.saga.ts +++ b/packages/mobile/src/store/root.saga.ts @@ -9,6 +9,7 @@ import { clearReduxStore } from './nativeServices/leaveCommunity/leaveCommunity. import { pushNotificationsMasterSaga } from './pushNotifications/pushNotifications.master.saga' import { setEngine, CryptoEngine } from 'pkijs' import { createLogger } from '../utils/logger' +import { keysMasterSaga } from './keys/keys.master.saga' const logger = createLogger('root') @@ -54,6 +55,7 @@ function* storeReadySaga(): Generator { fork(navigationMasterSaga), fork(nativeServicesMasterSaga), fork(pushNotificationsMasterSaga), + fork(keysMasterSaga), // Below line is reponsible for displaying notifications about messages from channels other than currently viewing one takeEvery(publicChannels.actions.markUnreadChannel.type, showNotificationSaga), takeLeading(initActions.canceledRootTask.type, clearReduxStore), diff --git a/packages/mobile/src/store/store.keys.ts b/packages/mobile/src/store/store.keys.ts index fdb67eed97..9389713392 100644 --- a/packages/mobile/src/store/store.keys.ts +++ b/packages/mobile/src/store/store.keys.ts @@ -3,4 +3,5 @@ export enum StoreKeys { Navigation = 'Navigation', NativeServices = 'NativeServices', PushNotifications = 'PushNotifications', + Keys = 'Keys', } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 0817d07065..4cdfcdaec1 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -13,4 +13,5 @@ export * from './network' export * from './test' export * from './captcha' export * from './serializer' +export * from './keys' export * from './notification' diff --git a/packages/types/src/keys.ts b/packages/types/src/keys.ts new file mode 100644 index 0000000000..7939aac759 --- /dev/null +++ b/packages/types/src/keys.ts @@ -0,0 +1,10 @@ +import { Base58, KeyMetadata } from '@localfirst/crdx' + +export interface StorableKey { + keyName: string + key: string | Base58 +} + +export interface KeysUpdatedEvent { + keys: StorableKey[] +} diff --git a/packages/types/src/socket.ts b/packages/types/src/socket.ts index f3cead2d8e..c518eed7e6 100644 --- a/packages/types/src/socket.ts +++ b/packages/types/src/socket.ts @@ -39,6 +39,7 @@ import { } from './community' import { ErrorPayload } from './errors' import { HCaptchaChallengeRequest, HCaptchaFormResponse, HCaptchaRequest } from './captcha' +import { KeysUpdatedEvent } from './keys' // ----------------------------------------------------------------------------- // SocketActions: These are the actions the frontend emits to the backend @@ -131,6 +132,7 @@ export enum SocketEvents { USERS_UPDATED = 'usersUpdated', USERS_REMOVED = 'usersRemoved', USER_PROFILES_STORED = 'userProfilesStored', + KEYS_UPDATED = 'keysUpdated', // ====== Files ====== FILE_ATTACHED = 'fileUploaded', @@ -235,6 +237,7 @@ export interface SocketEventsMap { [SocketEvents.USERS_UPDATED]: EmitEvent [SocketEvents.USERS_REMOVED]: EmitEvent [SocketEvents.USER_PROFILES_STORED]: EmitEvent + [SocketEvents.KEYS_UPDATED]: EmitEvent // ====== Files ====== [SocketEvents.FILE_ATTACHED]: EmitEvent