From 54df79c718cbb14b6a12e4a25f942381c4b383e5 Mon Sep 17 00:00:00 2001 From: Isla <5048549+islathehut@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:53:50 -0500 Subject: [PATCH 01/17] add NSE target and add keychain capability to NSE and Quiet --- .../ios/NotificationAppExtension/Info.plist | 13 ++ .../NotificationAppExtension.entitlements | 10 + .../NotificationService.swift | 35 ++++ .../ios/Quiet.xcodeproj/project.pbxproj | 187 +++++++++++++++++- packages/mobile/ios/Quiet/Quiet.entitlements | 4 + .../mobile/ios/Quiet/QuietDebug.entitlements | 4 + 6 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 packages/mobile/ios/NotificationAppExtension/Info.plist create mode 100644 packages/mobile/ios/NotificationAppExtension/NotificationAppExtension.entitlements create mode 100644 packages/mobile/ios/NotificationAppExtension/NotificationService.swift diff --git a/packages/mobile/ios/NotificationAppExtension/Info.plist b/packages/mobile/ios/NotificationAppExtension/Info.plist new file mode 100644 index 0000000000..57421ebf9b --- /dev/null +++ b/packages/mobile/ios/NotificationAppExtension/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + + diff --git a/packages/mobile/ios/NotificationAppExtension/NotificationAppExtension.entitlements b/packages/mobile/ios/NotificationAppExtension/NotificationAppExtension.entitlements new file mode 100644 index 0000000000..7ac57ebda7 --- /dev/null +++ b/packages/mobile/ios/NotificationAppExtension/NotificationAppExtension.entitlements @@ -0,0 +1,10 @@ + + + + + keychain-access-groups + + $(AppIdentifierPrefix)com.quietmobile + + + diff --git a/packages/mobile/ios/NotificationAppExtension/NotificationService.swift b/packages/mobile/ios/NotificationAppExtension/NotificationService.swift new file mode 100644 index 0000000000..0f337992d4 --- /dev/null +++ b/packages/mobile/ios/NotificationAppExtension/NotificationService.swift @@ -0,0 +1,35 @@ +// +// NotificationService.swift +// NotificationAppExtension +// +// Created by Isla Koenigsknecht on 2/18/26. +// + +import UserNotifications + +class NotificationService: UNNotificationServiceExtension { + + var contentHandler: ((UNNotificationContent) -> Void)? + var bestAttemptContent: UNMutableNotificationContent? + + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + self.contentHandler = contentHandler + bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) + + if let bestAttemptContent = bestAttemptContent { + // Modify the notification content here... + bestAttemptContent.title = "\(bestAttemptContent.title) [modified]" + + contentHandler(bestAttemptContent) + } + } + + override func serviceExtensionTimeWillExpire() { + // Called just before the extension will be terminated by the system. + // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. + if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { + contentHandler(bestAttemptContent) + } + } + +} diff --git a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj index 79efc9d819..9f813eb6a8 100644 --- a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -56,6 +56,7 @@ 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 */; }; + 665E5BC42F46402C005D2086 /* NotificationAppExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 665E5BBD2F46402C005D2086 /* NotificationAppExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 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 */; }; @@ -69,6 +70,13 @@ remoteGlobalIDString = 13B07F861A680F5B00A75B9A; remoteInfo = QuietMobile; }; + 665E5BC22F46402C005D2086 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 665E5BBC2F46402C005D2086; + remoteInfo = NotificationAppExtension; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -84,6 +92,17 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + 665E5BC52F46402D005D2086 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 665E5BC42F46402C005D2086 /* NotificationAppExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -621,12 +640,27 @@ 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 = ""; }; + 665E5BBD2F46402C005D2086 /* NotificationAppExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationAppExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; 84F12DFE2A5B0E05C2C41286 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Quiet/PrivacyInfo.xcprivacy; 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 = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 665E5BC82F46402D005D2086 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 665E5BBC2F46402C005D2086 /* NotificationAppExtension */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 665E5BBE2F46402C005D2086 /* NotificationAppExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (665E5BC82F46402D005D2086 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = NotificationAppExtension; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 00E356EB1AD99517003FC87E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -647,6 +681,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 665E5BBA2F46402C005D2086 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -4755,6 +4796,7 @@ 13B07FAE1A68108700A75B9A /* Quiet */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 00E356EF1AD99517003FC87E /* QuietTests */, + 665E5BBE2F46402C005D2086 /* NotificationAppExtension */, 83CBBA001A601CBA00E9B192 /* Products */, 2D16E6871FA4F8E400B85C8A /* Frameworks */, 624523FCC5994B7E9869E9CF /* Resources */, @@ -4770,6 +4812,7 @@ children = ( 13B07F961A680F5B00A75B9A /* Quiet.app */, 00E356EE1AD99517003FC87E /* QuietTests.xctest */, + 665E5BBD2F46402C005D2086 /* NotificationAppExtension.appex */, ); name = Products; sourceTree = ""; @@ -4816,16 +4859,40 @@ 1868C095292F8FE2001D6D5E /* Embed Frameworks */, 7253A52F091FC8122D730B2E /* [CP] Embed Pods Frameworks */, A1AE2657AE5C40D18941D671 /* [CP] Copy Pods Resources */, + 665E5BC52F46402D005D2086 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + 665E5BC32F46402C005D2086 /* PBXTargetDependency */, ); name = Quiet; productName = QuietMobile; productReference = 13B07F961A680F5B00A75B9A /* Quiet.app */; productType = "com.apple.product-type.application"; }; + 665E5BBC2F46402C005D2086 /* NotificationAppExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 665E5BC92F46402D005D2086 /* Build configuration list for PBXNativeTarget "NotificationAppExtension" */; + buildPhases = ( + 665E5BB92F46402C005D2086 /* Sources */, + 665E5BBA2F46402C005D2086 /* Frameworks */, + 665E5BBB2F46402C005D2086 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 665E5BBE2F46402C005D2086 /* NotificationAppExtension */, + ); + name = NotificationAppExtension; + packageProductDependencies = ( + ); + productName = NotificationAppExtension; + productReference = 665E5BBD2F46402C005D2086 /* NotificationAppExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -4833,6 +4900,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 1620; LastUpgradeCheck = 2610; TargetAttributes = { 00E356ED1AD99517003FC87E = { @@ -4842,6 +4910,9 @@ 13B07F861A680F5B00A75B9A = { LastSwiftMigration = 1250; }; + 665E5BBC2F46402C005D2086 = { + CreatedOnToolsVersion = 16.2; + }; }; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Quiet" */; @@ -4859,6 +4930,7 @@ targets = ( 13B07F861A680F5B00A75B9A /* Quiet */, 00E356ED1AD99517003FC87E /* QuietTests */, + 665E5BBC2F46402C005D2086 /* NotificationAppExtension */, ); }; /* End PBXProject section */ @@ -4909,6 +4981,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 665E5BBB2F46402C005D2086 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -5182,6 +5261,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 665E5BB92F46402C005D2086 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -5190,6 +5276,11 @@ target = 13B07F861A680F5B00A75B9A /* Quiet */; targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; }; + 665E5BC32F46402C005D2086 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 665E5BBC2F46402C005D2086 /* NotificationAppExtension */; + targetProxy = 665E5BC22F46402C005D2086 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -5453,6 +5544,91 @@ }; name = Release; }; + 665E5BC62F46402D005D2086 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = NotificationAppExtension/NotificationAppExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = CTYKSWN9T4; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NotificationAppExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NotificationAppExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.quietmobile.NotificationAppExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 665E5BC72F46402D005D2086 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = NotificationAppExtension/NotificationAppExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = CTYKSWN9T4; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NotificationAppExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NotificationAppExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.quietmobile.NotificationAppExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 83CBBA201A601CBA00E9B192 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -5622,6 +5798,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 665E5BC92F46402D005D2086 /* Build configuration list for PBXNativeTarget "NotificationAppExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 665E5BC62F46402D005D2086 /* Debug */, + 665E5BC72F46402D005D2086 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Quiet" */ = { isa = XCConfigurationList; buildConfigurations = ( 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 + From 97f1731b7688f83b88be3cba9bc2c946f72ed366 Mon Sep 17 00:00:00 2001 From: Isla <5048549+islathehut@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:37:56 -0500 Subject: [PATCH 02/17] Pass keys from backend to state manager and write to keychain --- .../auth/services/crypto/crypto.service.ts | 19 +- .../backend/src/nest/auth/sigchain.service.ts | 67 ++++++- packages/mobile/ios/CommunicationModule.swift | 15 ++ packages/mobile/ios/KeychainHandler.swift | 188 ++++++++++++++++++ .../ios/Quiet.xcodeproj/project.pbxproj | 4 + .../mobile/src/store/keys/keys.master.saga.ts | 20 ++ .../mobile/src/store/keys/keys.selectors..ts | 15 ++ packages/mobile/src/store/keys/keys.slice.ts | 28 +++ .../mobile/src/store/keys/keys.transform.ts | 14 ++ packages/mobile/src/store/keys/keys.type.ts | 11 + .../saveKeysInKeychain.saga.ts | 64 ++++++ packages/mobile/src/store/root.reducer.ts | 2 + packages/mobile/src/store/root.saga.ts | 2 + packages/mobile/src/store/store.keys.ts | 1 + packages/types/src/index.ts | 1 + packages/types/src/keys.ts | 12 ++ packages/types/src/socket.ts | 3 + 17 files changed, 457 insertions(+), 9 deletions(-) create mode 100644 packages/mobile/ios/KeychainHandler.swift create mode 100644 packages/mobile/src/store/keys/keys.master.saga.ts create mode 100644 packages/mobile/src/store/keys/keys.selectors..ts create mode 100644 packages/mobile/src/store/keys/keys.slice.ts create mode 100644 packages/mobile/src/store/keys/keys.transform.ts create mode 100644 packages/mobile/src/store/keys/keys.type.ts create mode 100644 packages/mobile/src/store/keys/saveKeysInKeychain/saveKeysInKeychain.saga.ts create mode 100644 packages/types/src/keys.ts 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..d197aa98c6 100644 --- a/packages/backend/src/nest/auth/sigchain.service.ts +++ b/packages/backend/src/nest/auth/sigchain.service.ts @@ -1,6 +1,17 @@ -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' @@ -10,12 +21,11 @@ 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 { KeysUpdatedEvent, KeyWithMetadata } from 'packages/types/src/keys' @Injectable() export class SigChainService extends EventEmitter { @@ -23,6 +33,7 @@ export class SigChainService extends EventEmitter { private readonly logger = createLogger(SigChainService.name) private chains: Map = new Map() public connections: Map = new Map() + private lastUpdatedLink: Hash constructor( @Inject(SERVER_IO_PROVIDER) public readonly serverIoProvider: ServerIoProviderTypes, @@ -133,6 +144,14 @@ export class SigChainService extends EventEmitter { } private handleChainUpdate = () => { + this._updateUsersOnChainUpdate() + this._updateKeysOnChainUpdate() + this.emit('updated') + this.saveChain(this.activeChainTeamName!) + this.logger.info('Chain updated, emitted updated event') + } + + private _updateUsersOnChainUpdate() { const users = this.getActiveChain() .team?.members() .map(user => ({ @@ -141,10 +160,42 @@ 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 }) + } + + // TODO: only fetch keys that have been updated recently + private _updateKeysOnChainUpdate() { + const secretKeys: KeyWithMetadata[] = [] + const sigKeys: KeyWithMetadata[] = [] + const userPublicKeys: KeyWithMetadata[] = [] + const allKeys = this.getActiveChain().crypto.getAllKeys() + for (const keyData of Object.values(allKeys)) { + for (const keyTypeData of Object.values(keyData)) { + for (const keyTypeGenData of Object.values(keyTypeData)) { + secretKeys.push({ + scope: { name: keyTypeGenData.name, type: keyTypeGenData.type, generation: keyTypeGenData.generation }, + key: keyTypeGenData.secretKey, + }) + } + } + } + // TODO: update to pull all generations of user public/sig keys + const allUserPublicKeys = this.getActiveChain().crypto.getPublicKeysForAllMembers(false) + for (const keySet of allUserPublicKeys) { + userPublicKeys.push({ + scope: { name: keySet.name, type: keySet.type, generation: keySet.generation }, + key: keySet.encryption, + }) + sigKeys.push({ + scope: { name: keySet.name, type: keySet.type, generation: keySet.generation }, + key: keySet.signature, + }) + } + this.serverIoProvider.io.emit(SocketEvents.KEYS_UPDATED, { + secretKeys, + sigKeys, + userPublicKeys, + } as KeysUpdatedEvent) } private attachSocketListeners(chain: SigChain): void { diff --git a/packages/mobile/ios/CommunicationModule.swift b/packages/mobile/ios/CommunicationModule.swift index f6961cdbfe..b579d2e964 100644 --- a/packages/mobile/ios/CommunicationModule.swift +++ b/packages/mobile/ios/CommunicationModule.swift @@ -12,6 +12,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) { @@ -56,6 +59,18 @@ class CommunicationModule: RCTEventEmitter { } } + @objc + func saveKeysInKeychain(newKeys: KeyWithScope[]) { + CommunicationModule.logger.debug("Saving \(newKeys.count) keys in keychain") + for key in newKeys { + do { + self.keychainHandler.addLfaKey(scope: key.scope, key: key.key) + } catch { + CommunicationModule.logger.error("Error while saving key in keychain", error) + } + } + } + @objc func checkNotificationPermission() { UNUserNotificationCenter.current().getNotificationSettings { settings in diff --git a/packages/mobile/ios/KeychainHandler.swift b/packages/mobile/ios/KeychainHandler.swift new file mode 100644 index 0000000000..49a928e70c --- /dev/null +++ b/packages/mobile/ios/KeychainHandler.swift @@ -0,0 +1,188 @@ +//import CryptoKit +//import Security +//import CoreData + +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 struct KeyScope { + var name: String + var generation: Int + var type: String + var keyType: String +} + +public enum KeyAddStatus { + case success + case duplicateScope +} + +public struct KeyWithScope { + var scope: KeyScope + var key: String +} + +@objc(KeychainHandler) +class KeychainHandler: NSObject { + private let masterKeyName: String = "quiet_master_key" + private let keychainGroupName: String = "com.quietmobile" + + public func getMasterKey() throws -> SymmetricKey { + do { + let password: String = try _getKeyImpl(keyName: masterKeyName) + let passwordBytes: ContiguousBytes = try _stringToBytes(str: password) + return SymmetricKey(data: passwordBytes) + } 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 getLfaKeyString(scope: KeyScope) throws -> String { + do { + let keyName: String = _keyScopeToKeyName(scope: scope) + 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 createMasterKey() throws -> SymmetricKey { + var existingKey: SymmetricKey? + do { + existingKey = try getMasterKey() + } catch KeychainHandlerError.noKeyFound { + existingKey = nil + } catch { + throw error + } + + guard existingKey == nil else { return existingKey! } + do { + let newKey: SymmetricKey = _generateAESKey() + let keyData: Data = _symmetricKeyToData(key: newKey) + let addStatus: KeyAddStatus = try _addKeyToKeychainImpl(keyName: masterKeyName, keyData: keyData) + guard addStatus == KeyAddStatus.success else { throw KeychainHandlerError.unhandledError(reason: addStatus) } + return newKey + } catch { + throw KeychainHandlerError.unhandledError(reason: error) + } + } + + public func addLfaKey(scope: KeyScope, key: String) throws -> KeyAddStatus { + var existingKey: String? + do { + existingKey = try getLfaKeyString(scope: scope) + } catch KeychainHandlerError.noKeyFound { + existingKey = nil + } catch KeychainHandlerError.malformedKey { + existingKey = nil + } catch { + throw error + } + + guard existingKey == nil else { + guard existingKey == key else { return KeyAddStatus.duplicateScope } + return KeyAddStatus.success + } + + do { + let keyName: String = _keyScopeToKeyName(scope: scope) + let keyData: Data = try _stringToBytes(str: key) + let addStatus: KeyAddStatus = try _addKeyToKeychainImpl(keyName: 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: kSecSharedPassword, + 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), + let account = existingItem[kSecAttrAccount as String] as? String + 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 _generateAESKey() -> SymmetricKey { + let key: SymmetricKey = SymmetricKey(size: .bits256) + return key + } + + private func _stringToBytes(str: String) throws -> Data { + let bytes: Data? = str.data(using: .utf8) + guard bytes != nil else { throw ConversionError.stringToBytesError } + return bytes! + } + + private func _symmetricKeyToData(key: SymmetricKey) -> Data { + let keyData: Data = key.withUnsafeBytes { body in + Data(body) + } + return keyData + } + + private func _keyScopeToKeyName(scope: KeyScope) -> String { + return "quiet_\(scope.type)_\(scope.name)_\(scope.generation)_\(scope.keyType)" + } +} diff --git a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj index 9f813eb6a8..3503c544f8 100644 --- a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj @@ -57,6 +57,7 @@ 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 */; }; 665E5BC42F46402C005D2086 /* NotificationAppExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 665E5BBD2F46402C005D2086 /* NotificationAppExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 6681DD3A2F4F53BF005D2086 /* KeychainHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6681DD392F4F53BF005D2086 /* KeychainHandler.swift */; }; 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 */; }; @@ -641,6 +642,7 @@ 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 = ""; }; 665E5BBD2F46402C005D2086 /* NotificationAppExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationAppExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 6681DD392F4F53BF005D2086 /* KeychainHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHandler.swift; 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 = ""; }; 84F12DFE2A5B0E05C2C41286 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Quiet/PrivacyInfo.xcprivacy; sourceTree = ""; }; 955DC7572BD930B30014725B /* WebsocketSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsocketSingleton.swift; sourceTree = ""; }; @@ -711,6 +713,7 @@ 13B07FAE1A68108700A75B9A /* Quiet */ = { isa = PBXGroup; children = ( + 6681DD392F4F53BF005D2086 /* KeychainHandler.swift */, 180E120A2AEFB7F900804659 /* Utils.swift */, 18FD2A36296F009E00A2B8C0 /* AppDelegate.h */, 18FD2A37296F009E00A2B8C0 /* AppDelegate.m */, @@ -5253,6 +5256,7 @@ 1868BCEE292E9212001D6D5E /* RNNodeJsMobile.m in Sources */, 1868C4382930D7D6001D6D5E /* DataDirectory.swift in Sources */, 1868C43E2930EAEA001D6D5E /* CommunicationBridge.m in Sources */, + 6681DD3A2F4F53BF005D2086 /* KeychainHandler.swift in Sources */, 1868C43A2930D859001D6D5E /* FindFreePort.swift in Sources */, 18FD2A3E296F009E00A2B8C0 /* AppDelegate.m in Sources */, 1868BCED292E9212001D6D5E /* NodeRunner.mm in Sources */, 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..e6016ba4c7 --- /dev/null +++ b/packages/mobile/src/store/keys/keys.selectors..ts @@ -0,0 +1,15 @@ +import { createSelector } from 'reselect' +import { StoreKeys } from '../store.keys' +import { CreatedSelectors, StoreState } from '../store.types' + +const keysSlice: CreatedSelectors[StoreKeys.Keys] = (state: StoreState) => state[StoreKeys.Keys] + +export const allKeys = createSelector(keysSlice, state => ({ + secretKeys: state.secretKeys, + userPublicKeys: state.userPublicKeys, + sigKeys: state.sigKeys, +})) + +export const keysSelectors = { + allKeys, +} 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..c69ef0ec9b --- /dev/null +++ b/packages/mobile/src/store/keys/keys.slice.ts @@ -0,0 +1,28 @@ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit' +import { StoreKeys } from '../store.keys' +import { KeysUpdatedEvent, KeyWithMetadata } from '@quiet/types' +import { createLogger } from '../../utils/logger' + +const logger = createLogger('keysSlice') + +export class KeysState { + public secretKeys: KeyWithMetadata[] = [] + public userPublicKeys: KeyWithMetadata[] = [] + public sigKeys: KeyWithMetadata[] = [] +} + +export const keysSlice = createSlice({ + initialState: { ...new KeysState() }, + name: StoreKeys.Keys, + reducers: { + setKeys: (state, action: PayloadAction) => { + state.secretKeys = action.payload.secretKeys + state.sigKeys = action.payload.sigKeys + state.userPublicKeys = action.payload.userPublicKeys + }, + 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..b0e994fcfa --- /dev/null +++ b/packages/mobile/src/store/keys/keys.type.ts @@ -0,0 +1,11 @@ +export type ExtendedKeyScope = { + type: string + name: string + generation: number + keyType: string +} + +export interface StorableKey { + scope: ExtendedKeyScope + key: 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..b669b966aa --- /dev/null +++ b/packages/mobile/src/store/keys/saveKeysInKeychain/saveKeysInKeychain.saga.ts @@ -0,0 +1,64 @@ +import { type PayloadAction } from '@reduxjs/toolkit' +import { call, select, put } from 'typed-redux-saga' +import { KeysUpdatedEvent } from '@quiet/types' +import { createLogger } from '../../../utils/logger' +import { keysActions } from '../keys.slice' +import { keysSelectors } from '../keys.selectors.' + +import _ from 'lodash' +import { NativeModules } from 'react-native' +import { StorableKey } from '../keys.type' + +const logger = createLogger('saveKeysInKeychainSaga') + +export function* saveKeysInKeychainSaga(action: PayloadAction): Generator { + logger.debug('Storing keys in ios keychain') + const existingKeys = yield* select(keysSelectors.allKeys) + const newSecretKeys = _.differenceBy(action.payload.secretKeys, existingKeys.secretKeys, 'key') + const newUserPublicKeys = _.differenceBy(action.payload.userPublicKeys, existingKeys.userPublicKeys, 'key') + const newSigKeys = _.differenceBy(action.payload.sigKeys, existingKeys.sigKeys, 'key') + logger.debug('Updating keys state') + yield* put(keysActions.setKeys(action.payload)) + + const newKeysPayload: KeysUpdatedEvent = { + secretKeys: newSecretKeys, + userPublicKeys: newUserPublicKeys, + sigKeys: newSigKeys, + } + const keysToSave: StorableKey[] = newSecretKeys.map( + keyWithMetadata => + ({ + scope: { + ...keyWithMetadata.scope, + keyType: 'secret', + }, + key: keyWithMetadata.key, + } as StorableKey) + ) + keysToSave.push( + ...newUserPublicKeys.map( + keyWithMetadata => + ({ + scope: { + ...keyWithMetadata.scope, + keyType: 'userPublic', + }, + key: keyWithMetadata.key, + } as StorableKey) + ) + ) + keysToSave.push( + ...newSigKeys.map( + keyWithMetadata => + ({ + scope: { + ...keyWithMetadata.scope, + keyType: 'userSig', + }, + key: keyWithMetadata.key, + } as StorableKey) + ) + ) + logger.debug('Putting new keys in keychain', keysToSave) + yield* call(NativeModules.CommunicationModule.saveKeysInKeychain, newKeysPayload) +} 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 2d92a64467..a795c26e15 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -13,3 +13,4 @@ export * from './network' export * from './test' export * from './captcha' export * from './serializer' +export * from './keys' diff --git a/packages/types/src/keys.ts b/packages/types/src/keys.ts new file mode 100644 index 0000000000..95e24fe0e2 --- /dev/null +++ b/packages/types/src/keys.ts @@ -0,0 +1,12 @@ +import { Base58, KeyMetadata } from '@localfirst/crdx' + +export interface KeyWithMetadata { + scope: KeyMetadata + key: string | Base58 +} + +export interface KeysUpdatedEvent { + secretKeys: KeyWithMetadata[] + userPublicKeys: KeyWithMetadata[] + sigKeys: KeyWithMetadata[] +} diff --git a/packages/types/src/socket.ts b/packages/types/src/socket.ts index d6ac8999f0..e1a7e0ec6a 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 @@ -128,6 +129,7 @@ export enum SocketEvents { USERS_UPDATED = 'usersUpdated', USERS_REMOVED = 'usersRemoved', USER_PROFILES_STORED = 'userProfilesStored', + KEYS_UPDATED = 'keysUpdated', // ====== Files ====== FILE_ATTACHED = 'fileUploaded', @@ -229,6 +231,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 From 7700b4f2077b08ca2f7bfbdfe4a80a4906542bb4 Mon Sep 17 00:00:00 2001 From: Isla <5048549+islathehut@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:02:23 -0500 Subject: [PATCH 03/17] Properly store keys in ios keychain --- .../backend/src/nest/auth/sigchain.service.ts | 20 +- packages/mobile/ios/CommunicationBridge.m | 1 + packages/mobile/ios/CommunicationModule.swift | 18 +- packages/mobile/ios/KeychainHandler.swift | 60 ++-- .../ios/NotificationAppExtension/Info.plist | 13 - .../NotificationAppExtension.entitlements | 10 - .../NotificationService.swift | 35 --- .../ios/Quiet.xcodeproj/project.pbxproj | 287 ++++-------------- .../startConnection/startConnection.saga.ts | 11 +- packages/mobile/src/store/keys/keys.type.ts | 1 + .../saveKeysInKeychain.saga.ts | 75 +++-- packages/types/src/keys.ts | 1 + 12 files changed, 162 insertions(+), 370 deletions(-) delete mode 100644 packages/mobile/ios/NotificationAppExtension/Info.plist delete mode 100644 packages/mobile/ios/NotificationAppExtension/NotificationAppExtension.entitlements delete mode 100644 packages/mobile/ios/NotificationAppExtension/NotificationService.swift diff --git a/packages/backend/src/nest/auth/sigchain.service.ts b/packages/backend/src/nest/auth/sigchain.service.ts index d197aa98c6..1b8392425d 100644 --- a/packages/backend/src/nest/auth/sigchain.service.ts +++ b/packages/backend/src/nest/auth/sigchain.service.ts @@ -11,7 +11,7 @@ import { UserWithSecrets, DeviceWithSecrets, } from '@localfirst/auth' -import { KeyMetadata } from '@localfirst/crdx' +import { KeyMetadata, KeyType } from '@localfirst/crdx' import { LocalDbService } from '../local-db/local-db.service' import { createLogger } from '../common/logger' import { SocketService } from '../socket/socket.service' @@ -25,7 +25,9 @@ import { SERVER_IO_PROVIDER } from '../const' import { ServerIoProviderTypes } from '../types' import EventEmitter from 'events' import { GetChainFilter } from './types' -import { KeysUpdatedEvent, KeyWithMetadata } from 'packages/types/src/keys' +import { KeysUpdatedEvent, KeyWithMetadata } from '@quiet/types' +import { EncryptionScopeType } from './services/crypto/types' +import * as os from 'os' @Injectable() export class SigChainService extends EventEmitter { @@ -165,6 +167,11 @@ export class SigChainService extends EventEmitter { // TODO: only fetch keys that have been updated recently private _updateKeysOnChainUpdate() { + if ((process.platform as string) !== 'ios') { + this.logger.trace('Skipping key update because we are not on ios, current platform =', process.platform) + return + } + const secretKeys: KeyWithMetadata[] = [] const sigKeys: KeyWithMetadata[] = [] const userPublicKeys: KeyWithMetadata[] = [] @@ -180,7 +187,7 @@ export class SigChainService extends EventEmitter { } } // TODO: update to pull all generations of user public/sig keys - const allUserPublicKeys = this.getActiveChain().crypto.getPublicKeysForAllMembers(false) + const allUserPublicKeys = this.getActiveChain().crypto.getPublicKeysForAllMembers(true) for (const keySet of allUserPublicKeys) { userPublicKeys.push({ scope: { name: keySet.name, type: keySet.type, generation: keySet.generation }, @@ -191,11 +198,13 @@ export class SigChainService extends EventEmitter { key: keySet.signature, }) } - this.serverIoProvider.io.emit(SocketEvents.KEYS_UPDATED, { + const keyUpdateEvent: KeysUpdatedEvent = { secretKeys, sigKeys, userPublicKeys, - } as KeysUpdatedEvent) + teamId: this.activeChain.team!.id, + } + this.serverIoProvider.io.emit(SocketEvents.KEYS_UPDATED, keyUpdateEvent) } private attachSocketListeners(chain: SigChain): void { @@ -259,6 +268,7 @@ export class SigChainService extends EventEmitter { const sigChain = SigChain.create(teamName, username) this.addChain(sigChain, setActive, teamName) await this.saveChain(teamName) + this.handleChainUpdate() return sigChain } 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 b579d2e964..71cb912914 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 { @@ -58,15 +59,20 @@ class CommunicationModule: RCTEventEmitter { } } } - + @objc - func saveKeysInKeychain(newKeys: KeyWithScope[]) { - CommunicationModule.logger.debug("Saving \(newKeys.count) keys in keychain") - for key in newKeys { + func saveKeysInKeychain(_ newKeys: NSArray) { + let decoder = JSONDecoder() + for keyAsAny in newKeys { do { - self.keychainHandler.addLfaKey(scope: key.scope, key: key.key) + let keyAsString: String = keyAsAny as! String + let data = Data(keyAsString.utf8) + let decodedKeyWithScope = try decoder.decode(KeyWithScope.self, from: data) + try self.keychainHandler.addLfaKey(keyWithScope: decodedKeyWithScope) + let stored = try self.keychainHandler.getLfaKeyString(teamId: decodedKeyWithScope.teamId, scope: decodedKeyWithScope.scope) + CommunicationModule.logger.info("Stored key matches? \(stored == decodedKeyWithScope.key) \(String(describing: decodedKeyWithScope.scope))") } catch { - CommunicationModule.logger.error("Error while saving key in keychain", error) + CommunicationModule.logger.error("Error while saving key in keychain: \(error)") } } } diff --git a/packages/mobile/ios/KeychainHandler.swift b/packages/mobile/ios/KeychainHandler.swift index 49a928e70c..cad13112bf 100644 --- a/packages/mobile/ios/KeychainHandler.swift +++ b/packages/mobile/ios/KeychainHandler.swift @@ -1,6 +1,15 @@ -//import CryptoKit -//import Security -//import CoreData +// +// 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 @@ -19,11 +28,11 @@ public enum KeychainHandlerError: Error { case unhandledError(reason: Any) } -public struct KeyScope { - var name: String - var generation: Int - var type: String - var keyType: String +public struct KeyScope: Codable { + let name: String + let generation: Int + let type: String + let keyType: String } public enum KeyAddStatus { @@ -31,15 +40,18 @@ public enum KeyAddStatus { case duplicateScope } -public struct KeyWithScope { - var scope: KeyScope - var key: String +public struct KeyWithScope: Codable { + let scope: KeyScope + let key: String + let teamId: String } @objc(KeychainHandler) class KeychainHandler: NSObject { private let masterKeyName: String = "quiet_master_key" private let keychainGroupName: String = "com.quietmobile" + + private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "KeychainHandler") public func getMasterKey() throws -> SymmetricKey { do { @@ -57,9 +69,9 @@ class KeychainHandler: NSObject { } } - public func getLfaKeyString(scope: KeyScope) throws -> String { + public func getLfaKeyString(teamId: String, scope: KeyScope) throws -> String { do { - let keyName: String = _keyScopeToKeyName(scope: scope) + let keyName: String = _keyScopeToKeyName(teamId: teamId, scope: scope) let password: String = try _getKeyImpl(keyName: keyName) return password } catch KeychainError.noPassword { @@ -95,26 +107,27 @@ class KeychainHandler: NSObject { } } - public func addLfaKey(scope: KeyScope, key: String) throws -> KeyAddStatus { + public func addLfaKey(keyWithScope: KeyWithScope) throws -> KeyAddStatus { var existingKey: String? do { - existingKey = try getLfaKeyString(scope: scope) + existingKey = try getLfaKeyString(teamId: keyWithScope.teamId, scope: keyWithScope.scope) } catch KeychainHandlerError.noKeyFound { existingKey = nil } catch KeychainHandlerError.malformedKey { existingKey = nil } catch { + KeychainHandler.logger.error("Error while getting existing LFA key for scope \(String(describing: keyWithScope.scope)): \(error)") throw error } guard existingKey == nil else { - guard existingKey == key else { return KeyAddStatus.duplicateScope } - return KeyAddStatus.success + guard existingKey == keyWithScope.key else { return KeyAddStatus.duplicateScope } + return KeyAddStatus.success } do { - let keyName: String = _keyScopeToKeyName(scope: scope) - let keyData: Data = try _stringToBytes(str: key) + let keyName: String = _keyScopeToKeyName(teamId: keyWithScope.teamId, scope: keyWithScope.scope) + let keyData: Data = try _stringToBytes(str: keyWithScope.key) let addStatus: KeyAddStatus = try _addKeyToKeychainImpl(keyName: keyName, keyData: keyData) return addStatus } catch { @@ -125,7 +138,7 @@ class KeychainHandler: NSObject { private func _getKeyImpl(keyName: String) throws -> String { var existingKey: CFTypeRef? let query: [String: Any] = [ - kSecClass as String: kSecSharedPassword, + kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: keychainGroupName, kSecAttrAccount as String: keyName, kSecMatchLimit as String: kSecMatchLimitOne, @@ -137,8 +150,7 @@ class KeychainHandler: NSObject { 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), - let account = existingItem[kSecAttrAccount as String] as? String + let password = String(data: passwordData, encoding: String.Encoding.utf8) else { throw KeychainError.unexpectedPasswordData } @@ -182,7 +194,7 @@ class KeychainHandler: NSObject { return keyData } - private func _keyScopeToKeyName(scope: KeyScope) -> String { - return "quiet_\(scope.type)_\(scope.name)_\(scope.generation)_\(scope.keyType)" + private func _keyScopeToKeyName(teamId: String, scope: KeyScope) -> String { + return "quiet_\(teamId)_\(scope.type)_\(scope.name)_\(scope.generation)_\(scope.keyType)" } } diff --git a/packages/mobile/ios/NotificationAppExtension/Info.plist b/packages/mobile/ios/NotificationAppExtension/Info.plist deleted file mode 100644 index 57421ebf9b..0000000000 --- a/packages/mobile/ios/NotificationAppExtension/Info.plist +++ /dev/null @@ -1,13 +0,0 @@ - - - - - NSExtension - - NSExtensionPointIdentifier - com.apple.usernotifications.service - NSExtensionPrincipalClass - $(PRODUCT_MODULE_NAME).NotificationService - - - diff --git a/packages/mobile/ios/NotificationAppExtension/NotificationAppExtension.entitlements b/packages/mobile/ios/NotificationAppExtension/NotificationAppExtension.entitlements deleted file mode 100644 index 7ac57ebda7..0000000000 --- a/packages/mobile/ios/NotificationAppExtension/NotificationAppExtension.entitlements +++ /dev/null @@ -1,10 +0,0 @@ - - - - - keychain-access-groups - - $(AppIdentifierPrefix)com.quietmobile - - - diff --git a/packages/mobile/ios/NotificationAppExtension/NotificationService.swift b/packages/mobile/ios/NotificationAppExtension/NotificationService.swift deleted file mode 100644 index 0f337992d4..0000000000 --- a/packages/mobile/ios/NotificationAppExtension/NotificationService.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// NotificationService.swift -// NotificationAppExtension -// -// Created by Isla Koenigsknecht on 2/18/26. -// - -import UserNotifications - -class NotificationService: UNNotificationServiceExtension { - - var contentHandler: ((UNNotificationContent) -> Void)? - var bestAttemptContent: UNMutableNotificationContent? - - override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { - self.contentHandler = contentHandler - bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - - if let bestAttemptContent = bestAttemptContent { - // Modify the notification content here... - bestAttemptContent.title = "\(bestAttemptContent.title) [modified]" - - contentHandler(bestAttemptContent) - } - } - - override func serviceExtensionTimeWillExpire() { - // Called just before the extension will be terminated by the system. - // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. - if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { - contentHandler(bestAttemptContent) - } - } - -} diff --git a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj index 3503c544f8..dc4625a9bb 100644 --- a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 70; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -55,12 +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 */; }; - 665E5BC42F46402C005D2086 /* NotificationAppExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 665E5BBD2F46402C005D2086 /* NotificationAppExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 6681DD3A2F4F53BF005D2086 /* KeychainHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6681DD392F4F53BF005D2086 /* KeychainHandler.swift */; }; + 36A82BC8FCE690814036F90F /* libPods-Quiet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 827250ED268E9BB42322847A /* libPods-Quiet.a */; }; + 665587CA2F4F5ECD005D2086 /* KeychainHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665587C92F4F5ECD005D2086 /* KeychainHandler.swift */; }; 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 */; }; + E527867D4810B57F475B8709 /* libPods-Quiet-QuietTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FFB5AD1B1D1B895AC31572D9 /* libPods-Quiet-QuietTests.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -71,13 +70,6 @@ remoteGlobalIDString = 13B07F861A680F5B00A75B9A; remoteInfo = QuietMobile; }; - 665E5BC22F46402C005D2086 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 665E5BBC2F46402C005D2086; - remoteInfo = NotificationAppExtension; - }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -93,17 +85,6 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - 665E5BC52F46402D005D2086 /* Embed Foundation Extensions */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 13; - files = ( - 665E5BC42F46402C005D2086 /* NotificationAppExtension.appex in Embed Foundation Extensions */, - ); - name = "Embed Foundation Extensions"; - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -112,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; }; @@ -127,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 = ""; }; @@ -640,36 +618,24 @@ 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 = ""; }; - 665E5BBD2F46402C005D2086 /* NotificationAppExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationAppExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - 6681DD392F4F53BF005D2086 /* KeychainHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHandler.swift; 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 = ""; }; + 665587C92F4F5ECD005D2086 /* KeychainHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHandler.swift; sourceTree = ""; }; + 827250ED268E9BB42322847A /* libPods-Quiet.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Quiet.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 84F12DFE2A5B0E05C2C41286 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Quiet/PrivacyInfo.xcprivacy; 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 = ""; }; + A0A640C5FB25A885E7EA3D28 /* 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 = ""; }; + A5B75BBA2D210014081EFE0C /* 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 = ""; }; + C7B1A71CADBC729F3E550DF1 /* 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 = ""; }; + FB3F0E6671064CAF2C159B5D /* 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 = ""; }; + FFB5AD1B1D1B895AC31572D9 /* libPods-Quiet-QuietTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Quiet-QuietTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ -/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 665E5BC82F46402D005D2086 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - Info.plist, - ); - target = 665E5BBC2F46402C005D2086 /* NotificationAppExtension */; - }; -/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ - -/* Begin PBXFileSystemSynchronizedRootGroup section */ - 665E5BBE2F46402C005D2086 /* NotificationAppExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (665E5BC82F46402D005D2086 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = NotificationAppExtension; sourceTree = ""; }; -/* End PBXFileSystemSynchronizedRootGroup section */ - /* Begin PBXFrameworksBuildPhase section */ 00E356EB1AD99517003FC87E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 1827A9E229783D6E00245FD3 /* classic-level.framework in Frameworks */, - 38AD376324628C6E27E70991 /* libPods-Quiet-QuietTests.a in Frameworks */, + E527867D4810B57F475B8709 /* libPods-Quiet-QuietTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -679,14 +645,7 @@ files = ( 00A416342EC2EAA900ACC877 /* NodeMobile.xcframework in Frameworks */, 1827A9E329783D7600245FD3 /* classic-level.framework in Frameworks */, - E479975299F0932FF690F805 /* libPods-Quiet.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 665E5BBA2F46402C005D2086 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( + 36A82BC8FCE690814036F90F /* libPods-Quiet.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -713,7 +672,7 @@ 13B07FAE1A68108700A75B9A /* Quiet */ = { isa = PBXGroup; children = ( - 6681DD392F4F53BF005D2086 /* KeychainHandler.swift */, + 665587C92F4F5ECD005D2086 /* KeychainHandler.swift */, 180E120A2AEFB7F900804659 /* Utils.swift */, 18FD2A36296F009E00A2B8C0 /* AppDelegate.h */, 18FD2A37296F009E00A2B8C0 /* AppDelegate.m */, @@ -4745,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 */, + A0A640C5FB25A885E7EA3D28 /* Pods-Quiet.debug.xcconfig */, + C7B1A71CADBC729F3E550DF1 /* Pods-Quiet.release.xcconfig */, + FB3F0E6671064CAF2C159B5D /* Pods-Quiet-QuietTests.debug.xcconfig */, + A5B75BBA2D210014081EFE0C /* Pods-Quiet-QuietTests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -4758,8 +4717,8 @@ children = ( 00A416332EC2EAA900ACC877 /* NodeMobile.xcframework */, 1827A9E129783D6E00245FD3 /* classic-level.framework */, - 104C0D0FA8EF00192C60CAD7 /* libPods-Quiet.a */, - 02AFA323BA7196A67E7A1133 /* libPods-Quiet-QuietTests.a */, + 827250ED268E9BB42322847A /* libPods-Quiet.a */, + FFB5AD1B1D1B895AC31572D9 /* libPods-Quiet-QuietTests.a */, ); name = Frameworks; sourceTree = ""; @@ -4799,7 +4758,6 @@ 13B07FAE1A68108700A75B9A /* Quiet */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 00E356EF1AD99517003FC87E /* QuietTests */, - 665E5BBE2F46402C005D2086 /* NotificationAppExtension */, 83CBBA001A601CBA00E9B192 /* Products */, 2D16E6871FA4F8E400B85C8A /* Frameworks */, 624523FCC5994B7E9869E9CF /* Resources */, @@ -4815,7 +4773,6 @@ children = ( 13B07F961A680F5B00A75B9A /* Quiet.app */, 00E356EE1AD99517003FC87E /* QuietTests.xctest */, - 665E5BBD2F46402C005D2086 /* NotificationAppExtension.appex */, ); name = Products; sourceTree = ""; @@ -4827,12 +4784,12 @@ isa = PBXNativeTarget; buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "QuietTests" */; buildPhases = ( - 58352AE7BD90BC0845FC78DE /* [CP] Check Pods Manifest.lock */, + 69006B3CE96CD13A5F5A5226 /* [CP] Check Pods Manifest.lock */, 00E356EA1AD99517003FC87E /* Sources */, 00E356EB1AD99517003FC87E /* Frameworks */, 00E356EC1AD99517003FC87E /* Resources */, - EF4E9879B3A5060ADF995D6A /* [CP] Embed Pods Frameworks */, - 4E18C6AEAD8D524EAD836F51 /* [CP] Copy Pods Resources */, + E49E02522FA800C59F2F014D /* [CP] Embed Pods Frameworks */, + C3AB99BB8D1CD10850476367 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -4848,7 +4805,7 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Quiet" */; buildPhases = ( - 715B735C8E7FA602A967EAF0 /* [CP] Check Pods Manifest.lock */, + 626C6584D894673F46A360BF /* [CP] Check Pods Manifest.lock */, FD10A7F022414F080027D42C /* Start Packager */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, @@ -4860,42 +4817,18 @@ 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 */, - 665E5BC52F46402D005D2086 /* Embed Foundation Extensions */, + D141249F8994850B1DC7FF46 /* [CP] Embed Pods Frameworks */, + 51FD2183E5DD9C7BD49270BD /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( - 665E5BC32F46402C005D2086 /* PBXTargetDependency */, ); name = Quiet; productName = QuietMobile; productReference = 13B07F961A680F5B00A75B9A /* Quiet.app */; productType = "com.apple.product-type.application"; }; - 665E5BBC2F46402C005D2086 /* NotificationAppExtension */ = { - isa = PBXNativeTarget; - buildConfigurationList = 665E5BC92F46402D005D2086 /* Build configuration list for PBXNativeTarget "NotificationAppExtension" */; - buildPhases = ( - 665E5BB92F46402C005D2086 /* Sources */, - 665E5BBA2F46402C005D2086 /* Frameworks */, - 665E5BBB2F46402C005D2086 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - fileSystemSynchronizedGroups = ( - 665E5BBE2F46402C005D2086 /* NotificationAppExtension */, - ); - name = NotificationAppExtension; - packageProductDependencies = ( - ); - productName = NotificationAppExtension; - productReference = 665E5BBD2F46402C005D2086 /* NotificationAppExtension.appex */; - productType = "com.apple.product-type.app-extension"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -4903,7 +4836,6 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 1620; LastUpgradeCheck = 2610; TargetAttributes = { 00E356ED1AD99517003FC87E = { @@ -4913,9 +4845,6 @@ 13B07F861A680F5B00A75B9A = { LastSwiftMigration = 1250; }; - 665E5BBC2F46402C005D2086 = { - CreatedOnToolsVersion = 16.2; - }; }; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Quiet" */; @@ -4933,7 +4862,6 @@ targets = ( 13B07F861A680F5B00A75B9A /* Quiet */, 00E356ED1AD99517003FC87E /* QuietTests */, - 665E5BBC2F46402C005D2086 /* NotificationAppExtension */, ); }; /* End PBXProject section */ @@ -4984,13 +4912,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 665E5BBB2F46402C005D2086 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -5006,7 +4927,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; @@ -5102,24 +5023,24 @@ shellPath = /bin/sh; shellScript = "find \"$CODESIGNING_FOLDER_PATH/nodejs-project/node_modules/\" -name \"python3\" | xargs rm\n"; }; - 4E18C6AEAD8D524EAD836F51 /* [CP] Copy Pods Resources */ = { + 51FD2183E5DD9C7BD49270BD /* [CP] Copy Pods Resources */ = { 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-resources-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Copy Pods Resources"; 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-resources-${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-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 58352AE7BD90BC0845FC78DE /* [CP] Check Pods Manifest.lock */ = { + 626C6584D894673F46A360BF /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -5134,14 +5055,14 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Quiet-QuietTests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Quiet-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; }; - 715B735C8E7FA602A967EAF0 /* [CP] Check Pods Manifest.lock */ = { + 69006B3CE96CD13A5F5A5226 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -5156,48 +5077,48 @@ 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 */ = { + C3AB99BB8D1CD10850476367 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-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/Pods-Quiet-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/Pods-Quiet-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-resources.sh\"\n"; showEnvVarsInLog = 0; }; - A1AE2657AE5C40D18941D671 /* [CP] Copy Pods Resources */ = { + D141249F8994850B1DC7FF46 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-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/Pods-Quiet-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/Pods-Quiet-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - EF4E9879B3A5060ADF995D6A /* [CP] Embed Pods Frameworks */ = { + E49E02522FA800C59F2F014D /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -5256,7 +5177,7 @@ 1868BCEE292E9212001D6D5E /* RNNodeJsMobile.m in Sources */, 1868C4382930D7D6001D6D5E /* DataDirectory.swift in Sources */, 1868C43E2930EAEA001D6D5E /* CommunicationBridge.m in Sources */, - 6681DD3A2F4F53BF005D2086 /* KeychainHandler.swift in Sources */, + 665587CA2F4F5ECD005D2086 /* KeychainHandler.swift in Sources */, 1868C43A2930D859001D6D5E /* FindFreePort.swift in Sources */, 18FD2A3E296F009E00A2B8C0 /* AppDelegate.m in Sources */, 1868BCED292E9212001D6D5E /* NodeRunner.mm in Sources */, @@ -5265,13 +5186,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 665E5BB92F46402C005D2086 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -5280,17 +5194,12 @@ target = 13B07F861A680F5B00A75B9A /* Quiet */; targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; }; - 665E5BC32F46402C005D2086 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 665E5BBC2F46402C005D2086 /* NotificationAppExtension */; - targetProxy = 665E5BC22F46402C005D2086 /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7F75CE6D1C7D3DA9F5BA6A76 /* Pods-Quiet-QuietTests.debug.xcconfig */; + baseConfigurationReference = FB3F0E6671064CAF2C159B5D /* Pods-Quiet-QuietTests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -5323,7 +5232,7 @@ }; 00E356F71AD99517003FC87E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 2877B6088D1D81EF869D71D9 /* Pods-Quiet-QuietTests.release.xcconfig */; + baseConfigurationReference = A5B75BBA2D210014081EFE0C /* Pods-Quiet-QuietTests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -5353,7 +5262,7 @@ }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E727FE0446E9E46CDECEC7FE /* Pods-Quiet.debug.xcconfig */; + baseConfigurationReference = A0A640C5FB25A885E7EA3D28 /* Pods-Quiet.debug.xcconfig */; buildSettings = { ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -5454,7 +5363,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 116749B7D5552021F04BE739 /* Pods-Quiet.release.xcconfig */; + baseConfigurationReference = C7B1A71CADBC729F3E550DF1 /* Pods-Quiet.release.xcconfig */; buildSettings = { ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -5548,91 +5457,6 @@ }; name = Release; }; - 665E5BC62F46402D005D2086 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = NotificationAppExtension/NotificationAppExtension.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = CTYKSWN9T4; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = NotificationAppExtension/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = NotificationAppExtension; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.quietmobile.NotificationAppExtension; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 665E5BC72F46402D005D2086 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = NotificationAppExtension/NotificationAppExtension.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = CTYKSWN9T4; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = NotificationAppExtension/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = NotificationAppExtension; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.quietmobile.NotificationAppExtension; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; 83CBBA201A601CBA00E9B192 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -5802,15 +5626,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 665E5BC92F46402D005D2086 /* Build configuration list for PBXNativeTarget "NotificationAppExtension" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 665E5BC62F46402D005D2086 /* Debug */, - 665E5BC72F46402D005D2086 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Quiet" */ = { isa = XCConfigurationList; buildConfigurations = ( 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.type.ts b/packages/mobile/src/store/keys/keys.type.ts index b0e994fcfa..5148b443a9 100644 --- a/packages/mobile/src/store/keys/keys.type.ts +++ b/packages/mobile/src/store/keys/keys.type.ts @@ -8,4 +8,5 @@ export type ExtendedKeyScope = { 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 index b669b966aa..3e34095ef3 100644 --- a/packages/mobile/src/store/keys/saveKeysInKeychain/saveKeysInKeychain.saga.ts +++ b/packages/mobile/src/store/keys/saveKeysInKeychain/saveKeysInKeychain.saga.ts @@ -12,53 +12,50 @@ import { StorableKey } from '../keys.type' const logger = createLogger('saveKeysInKeychainSaga') export function* saveKeysInKeychainSaga(action: PayloadAction): Generator { - logger.debug('Storing keys in ios keychain') + logger.info('Storing keys in ios keychain') const existingKeys = yield* select(keysSelectors.allKeys) const newSecretKeys = _.differenceBy(action.payload.secretKeys, existingKeys.secretKeys, 'key') const newUserPublicKeys = _.differenceBy(action.payload.userPublicKeys, existingKeys.userPublicKeys, 'key') const newSigKeys = _.differenceBy(action.payload.sigKeys, existingKeys.sigKeys, 'key') - logger.debug('Updating keys state') + logger.info('Updating keys state') yield* put(keysActions.setKeys(action.payload)) - const newKeysPayload: KeysUpdatedEvent = { - secretKeys: newSecretKeys, - userPublicKeys: newUserPublicKeys, - sigKeys: newSigKeys, - } - const keysToSave: StorableKey[] = newSecretKeys.map( - keyWithMetadata => - ({ - scope: { - ...keyWithMetadata.scope, - keyType: 'secret', - }, - key: keyWithMetadata.key, - } as StorableKey) - ) + const keysToSave: StorableKey[] = newSecretKeys.map(keyWithMetadata => ({ + scope: { + ...keyWithMetadata.scope, + keyType: 'secret', + }, + key: keyWithMetadata.key, + teamId: action.payload.teamId, + })) keysToSave.push( - ...newUserPublicKeys.map( - keyWithMetadata => - ({ - scope: { - ...keyWithMetadata.scope, - keyType: 'userPublic', - }, - key: keyWithMetadata.key, - } as StorableKey) - ) + ...newUserPublicKeys.map(keyWithMetadata => ({ + scope: { + ...keyWithMetadata.scope, + keyType: 'userPublic', + }, + key: keyWithMetadata.key, + teamId: action.payload.teamId, + })) ) keysToSave.push( - ...newSigKeys.map( - keyWithMetadata => - ({ - scope: { - ...keyWithMetadata.scope, - keyType: 'userSig', - }, - key: keyWithMetadata.key, - } as StorableKey) - ) + ...newSigKeys.map(keyWithMetadata => ({ + scope: { + ...keyWithMetadata.scope, + keyType: 'userSig', + }, + key: keyWithMetadata.key, + teamId: action.payload.teamId, + })) ) - logger.debug('Putting new keys in keychain', keysToSave) - yield* call(NativeModules.CommunicationModule.saveKeysInKeychain, newKeysPayload) + + logger.info('Putting new keys in keychain', keysToSave) + try { + yield* call( + NativeModules.CommunicationModule.saveKeysInKeychain, + keysToSave.map(key => JSON.stringify(key)) + ) + } catch (e) { + logger.error('Error while updating keys on keychain', e) + } } diff --git a/packages/types/src/keys.ts b/packages/types/src/keys.ts index 95e24fe0e2..c4e78fcc98 100644 --- a/packages/types/src/keys.ts +++ b/packages/types/src/keys.ts @@ -9,4 +9,5 @@ export interface KeysUpdatedEvent { secretKeys: KeyWithMetadata[] userPublicKeys: KeyWithMetadata[] sigKeys: KeyWithMetadata[] + teamId: string } From 76565d75417bbcbbbb0396e204bceb2bbe51f2f4 Mon Sep 17 00:00:00 2001 From: Isla <5048549+islathehut@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:15:58 -0500 Subject: [PATCH 04/17] Don't store in state manager, simplify model that is sent to frontend, decrease size of stored send data --- .../backend/src/nest/auth/sigchain.service.ts | 63 +++++--- packages/backend/src/nest/auth/types.ts | 6 + .../src/nest/local-db/local-db.service.ts | 13 ++ .../src/nest/local-db/local-db.types.ts | 3 + packages/mobile/ios/CommunicationModule.swift | 8 +- packages/mobile/ios/KeychainHandler.swift | 32 ++--- .../ios/Quiet.xcodeproj/project.pbxproj | 136 +++++++++--------- .../mobile/src/store/keys/keys.selectors..ts | 11 +- packages/mobile/src/store/keys/keys.slice.ts | 13 +- .../saveKeysInKeychain.saga.ts | 43 +----- packages/types/src/keys.ts | 9 +- 11 files changed, 151 insertions(+), 186 deletions(-) diff --git a/packages/backend/src/nest/auth/sigchain.service.ts b/packages/backend/src/nest/auth/sigchain.service.ts index 1b8392425d..7870aa3d67 100644 --- a/packages/backend/src/nest/auth/sigchain.service.ts +++ b/packages/backend/src/nest/auth/sigchain.service.ts @@ -11,11 +11,11 @@ import { UserWithSecrets, DeviceWithSecrets, } from '@localfirst/auth' -import { KeyMetadata, KeyType } from '@localfirst/crdx' +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' @@ -24,10 +24,8 @@ import { type CryptoService } from './services/crypto/crypto.service' import { SERVER_IO_PROVIDER } from '../const' import { ServerIoProviderTypes } from '../types' import EventEmitter from 'events' -import { GetChainFilter } from './types' -import { KeysUpdatedEvent, KeyWithMetadata } from '@quiet/types' -import { EncryptionScopeType } from './services/crypto/types' -import * as os from 'os' +import { GetChainFilter, StoredKeyType } from './types' +import { KeysUpdatedEvent } from '@quiet/types' @Injectable() export class SigChainService extends EventEmitter { @@ -166,44 +164,63 @@ export class SigChainService extends EventEmitter { } // TODO: only fetch keys that have been updated recently - private _updateKeysOnChainUpdate() { + private async _updateKeysOnChainUpdate() { if ((process.platform as string) !== 'ios') { this.logger.trace('Skipping key update because we are not on ios, current platform =', process.platform) return } - const secretKeys: KeyWithMetadata[] = [] - const sigKeys: KeyWithMetadata[] = [] - const userPublicKeys: KeyWithMetadata[] = [] + const generateKeyName = (teamId: string, keyType: string, scope: KeyMetadata): string => { + return `quiet_${teamId}_${scope.type}_${scope.name}_${scope.generation}_${keyType}` + } + + const teamId = this.activeChain.team!.id + const alreadySentKeys: Set = new Set(await this.localDbService.getKeysStoredInKeychain(teamId)) + const keysToSend: StorableKey[] = [] + const keyNamesSent: string[] = [] const allKeys = this.getActiveChain().crypto.getAllKeys() for (const keyData of Object.values(allKeys)) { for (const keyTypeData of Object.values(keyData)) { for (const keyTypeGenData of Object.values(keyTypeData)) { - secretKeys.push({ - scope: { name: keyTypeGenData.name, type: keyTypeGenData.type, generation: keyTypeGenData.generation }, - key: keyTypeGenData.secretKey, + 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 const allUserPublicKeys = this.getActiveChain().crypto.getPublicKeysForAllMembers(true) for (const keySet of allUserPublicKeys) { - userPublicKeys.push({ - scope: { name: keySet.name, type: keySet.type, generation: keySet.generation }, - key: keySet.encryption, + const publicKeyName = generateKeyName(teamId, StoredKeyType.USER_PUBLIC, { + name: keySet.name, + type: keySet.type, + generation: keySet.generation, }) - sigKeys.push({ - scope: { name: keySet.name, type: keySet.type, generation: keySet.generation }, - key: keySet.signature, + 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) + } } const keyUpdateEvent: KeysUpdatedEvent = { - secretKeys, - sigKeys, - userPublicKeys, - teamId: this.activeChain.team!.id, + keys: keysToSend, } + await this.localDbService.updateKeysStoredInKeychain(teamId, keyNamesSent) this.serverIoProvider.io.emit(SocketEvents.KEYS_UPDATED, keyUpdateEvent) } 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..040c0af71d 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,17 @@ export class LocalDbService extends EventEmitter { } return count } + + 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) + } + + 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/mobile/ios/CommunicationModule.swift b/packages/mobile/ios/CommunicationModule.swift index 71cb912914..daa596255b 100644 --- a/packages/mobile/ios/CommunicationModule.swift +++ b/packages/mobile/ios/CommunicationModule.swift @@ -67,10 +67,10 @@ class CommunicationModule: RCTEventEmitter { do { let keyAsString: String = keyAsAny as! String let data = Data(keyAsString.utf8) - let decodedKeyWithScope = try decoder.decode(KeyWithScope.self, from: data) - try self.keychainHandler.addLfaKey(keyWithScope: decodedKeyWithScope) - let stored = try self.keychainHandler.getLfaKeyString(teamId: decodedKeyWithScope.teamId, scope: decodedKeyWithScope.scope) - CommunicationModule.logger.info("Stored key matches? \(stored == decodedKeyWithScope.key) \(String(describing: decodedKeyWithScope.scope))") + 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 { CommunicationModule.logger.error("Error while saving key in keychain: \(error)") } diff --git a/packages/mobile/ios/KeychainHandler.swift b/packages/mobile/ios/KeychainHandler.swift index cad13112bf..3b86f1e5e9 100644 --- a/packages/mobile/ios/KeychainHandler.swift +++ b/packages/mobile/ios/KeychainHandler.swift @@ -28,22 +28,14 @@ public enum KeychainHandlerError: Error { case unhandledError(reason: Any) } -public struct KeyScope: Codable { - let name: String - let generation: Int - let type: String - let keyType: String -} - public enum KeyAddStatus { case success case duplicateScope } -public struct KeyWithScope: Codable { - let scope: KeyScope +public struct NamedKey: Codable { + let keyName: String let key: String - let teamId: String } @objc(KeychainHandler) @@ -69,9 +61,8 @@ class KeychainHandler: NSObject { } } - public func getLfaKeyString(teamId: String, scope: KeyScope) throws -> String { + public func getLfaKeyString(keyName: String) throws -> String { do { - let keyName: String = _keyScopeToKeyName(teamId: teamId, scope: scope) let password: String = try _getKeyImpl(keyName: keyName) return password } catch KeychainError.noPassword { @@ -107,28 +98,27 @@ class KeychainHandler: NSObject { } } - public func addLfaKey(keyWithScope: KeyWithScope) throws -> KeyAddStatus { + public func addLfaKey(namedKey: NamedKey) throws -> KeyAddStatus { var existingKey: String? do { - existingKey = try getLfaKeyString(teamId: keyWithScope.teamId, scope: keyWithScope.scope) + 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 scope \(String(describing: keyWithScope.scope)): \(error)") + KeychainHandler.logger.error("Error while getting existing LFA key for name \(namedKey.keyName): \(error)") throw error } guard existingKey == nil else { - guard existingKey == keyWithScope.key else { return KeyAddStatus.duplicateScope } + guard existingKey == namedKey.key else { return KeyAddStatus.duplicateScope } return KeyAddStatus.success } do { - let keyName: String = _keyScopeToKeyName(teamId: keyWithScope.teamId, scope: keyWithScope.scope) - let keyData: Data = try _stringToBytes(str: keyWithScope.key) - let addStatus: KeyAddStatus = try _addKeyToKeychainImpl(keyName: keyName, keyData: keyData) + 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) @@ -193,8 +183,4 @@ class KeychainHandler: NSObject { } return keyData } - - private func _keyScopeToKeyName(teamId: String, scope: KeyScope) -> String { - return "quiet_\(teamId)_\(scope.type)_\(scope.name)_\(scope.generation)_\(scope.keyType)" - } } diff --git a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj index dc4625a9bb..e1d226dc60 100644 --- a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj @@ -55,11 +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 */; }; - 36A82BC8FCE690814036F90F /* libPods-Quiet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 827250ED268E9BB42322847A /* libPods-Quiet.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 */; }; - E527867D4810B57F475B8709 /* libPods-Quiet-QuietTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FFB5AD1B1D1B895AC31572D9 /* libPods-Quiet-QuietTests.a */; }; + FFD33AE2521CEA2E0D3134ED /* libPods-Quiet-QuietTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CA54D67D8C7A248C6A22C0F8 /* libPods-Quiet-QuietTests.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -618,15 +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 = ""; }; + 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 = ""; }; - 827250ED268E9BB42322847A /* libPods-Quiet.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Quiet.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; - A0A640C5FB25A885E7EA3D28 /* 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 = ""; }; - A5B75BBA2D210014081EFE0C /* 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 = ""; }; - C7B1A71CADBC729F3E550DF1 /* 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 = ""; }; - FB3F0E6671064CAF2C159B5D /* 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 = ""; }; - FFB5AD1B1D1B895AC31572D9 /* libPods-Quiet-QuietTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Quiet-QuietTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 */ @@ -635,7 +635,7 @@ buildActionMask = 2147483647; files = ( 1827A9E229783D6E00245FD3 /* classic-level.framework in Frameworks */, - E527867D4810B57F475B8709 /* libPods-Quiet-QuietTests.a in Frameworks */, + FFD33AE2521CEA2E0D3134ED /* libPods-Quiet-QuietTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -645,7 +645,7 @@ files = ( 00A416342EC2EAA900ACC877 /* NodeMobile.xcframework in Frameworks */, 1827A9E329783D7600245FD3 /* classic-level.framework in Frameworks */, - 36A82BC8FCE690814036F90F /* libPods-Quiet.a in Frameworks */, + 85137B01156B661E82184B11 /* libPods-Quiet.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4704,10 +4704,10 @@ 1CEEDB4F07B9978C125775C5 /* Pods */ = { isa = PBXGroup; children = ( - A0A640C5FB25A885E7EA3D28 /* Pods-Quiet.debug.xcconfig */, - C7B1A71CADBC729F3E550DF1 /* Pods-Quiet.release.xcconfig */, - FB3F0E6671064CAF2C159B5D /* Pods-Quiet-QuietTests.debug.xcconfig */, - A5B75BBA2D210014081EFE0C /* 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 = ""; @@ -4717,8 +4717,8 @@ children = ( 00A416332EC2EAA900ACC877 /* NodeMobile.xcframework */, 1827A9E129783D6E00245FD3 /* classic-level.framework */, - 827250ED268E9BB42322847A /* libPods-Quiet.a */, - FFB5AD1B1D1B895AC31572D9 /* libPods-Quiet-QuietTests.a */, + 287A03B12772D68AAF979A77 /* libPods-Quiet.a */, + CA54D67D8C7A248C6A22C0F8 /* libPods-Quiet-QuietTests.a */, ); name = Frameworks; sourceTree = ""; @@ -4784,12 +4784,12 @@ isa = PBXNativeTarget; buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "QuietTests" */; buildPhases = ( - 69006B3CE96CD13A5F5A5226 /* [CP] Check Pods Manifest.lock */, + 95F7BA6D16EB3A1B27BAA786 /* [CP] Check Pods Manifest.lock */, 00E356EA1AD99517003FC87E /* Sources */, 00E356EB1AD99517003FC87E /* Frameworks */, 00E356EC1AD99517003FC87E /* Resources */, - E49E02522FA800C59F2F014D /* [CP] Embed Pods Frameworks */, - C3AB99BB8D1CD10850476367 /* [CP] Copy Pods Resources */, + 911E369A1C6D85907F7F0302 /* [CP] Embed Pods Frameworks */, + E7F2776DE3547BE1502BC947 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -4805,7 +4805,7 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Quiet" */; buildPhases = ( - 626C6584D894673F46A360BF /* [CP] Check Pods Manifest.lock */, + BB9294C6673D9CCB22CBA0B8 /* [CP] Check Pods Manifest.lock */, FD10A7F022414F080027D42C /* Start Packager */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, @@ -4817,8 +4817,8 @@ 18FD2A32296D736300A2B8C0 /* [CUSTOM NODEJS MOBILE] Remove Python3 Binaries */, 1827A9E0297837FE00245FD3 /* [CUSTOM NODEJS MOBILE] Remove prebuilds */, 1868C095292F8FE2001D6D5E /* Embed Frameworks */, - D141249F8994850B1DC7FF46 /* [CP] Embed Pods Frameworks */, - 51FD2183E5DD9C7BD49270BD /* [CP] Copy Pods Resources */, + 85BBD2CD345055BC7227A72B /* [CP] Embed Pods Frameworks */, + 00462B5D3156F56526040199 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -4915,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; @@ -5023,46 +5040,41 @@ shellPath = /bin/sh; shellScript = "find \"$CODESIGNING_FOLDER_PATH/nodejs-project/node_modules/\" -name \"python3\" | xargs rm\n"; }; - 51FD2183E5DD9C7BD49270BD /* [CP] Copy Pods Resources */ = { + 85BBD2CD345055BC7227A72B /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-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/Pods-Quiet-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/Pods-Quiet-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 626C6584D894673F46A360BF /* [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-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; }; - 69006B3CE96CD13A5F5A5226 /* [CP] Check Pods Manifest.lock */ = { + 95F7BA6D16EB3A1B27BAA786 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -5084,55 +5096,43 @@ 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; }; - C3AB99BB8D1CD10850476367 /* [CP] Copy Pods Resources */ = { + BB9294C6673D9CCB22CBA0B8 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${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-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - D141249F8994850B1DC7FF46 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-frameworks-${CONFIGURATION}-input-files.xcfilelist", + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "[CP] Embed Pods Frameworks"; + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-frameworks-${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-frameworks.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; }; - E49E02522FA800C59F2F014D /* [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 */ = { @@ -5199,7 +5199,7 @@ /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FB3F0E6671064CAF2C159B5D /* Pods-Quiet-QuietTests.debug.xcconfig */; + baseConfigurationReference = 70BDAC54548DB3505251D3DB /* Pods-Quiet-QuietTests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -5232,7 +5232,7 @@ }; 00E356F71AD99517003FC87E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A5B75BBA2D210014081EFE0C /* Pods-Quiet-QuietTests.release.xcconfig */; + baseConfigurationReference = 88972EF0A58F6401DC66730D /* Pods-Quiet-QuietTests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -5262,7 +5262,7 @@ }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A0A640C5FB25A885E7EA3D28 /* Pods-Quiet.debug.xcconfig */; + baseConfigurationReference = 2F63F73503B4A62481768267 /* Pods-Quiet.debug.xcconfig */; buildSettings = { ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -5363,7 +5363,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C7B1A71CADBC729F3E550DF1 /* Pods-Quiet.release.xcconfig */; + baseConfigurationReference = 677CCEF15F36760C73BC26A6 /* Pods-Quiet.release.xcconfig */; buildSettings = { ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; diff --git a/packages/mobile/src/store/keys/keys.selectors..ts b/packages/mobile/src/store/keys/keys.selectors..ts index e6016ba4c7..647e429649 100644 --- a/packages/mobile/src/store/keys/keys.selectors..ts +++ b/packages/mobile/src/store/keys/keys.selectors..ts @@ -1,15 +1,6 @@ -import { createSelector } from 'reselect' import { StoreKeys } from '../store.keys' import { CreatedSelectors, StoreState } from '../store.types' const keysSlice: CreatedSelectors[StoreKeys.Keys] = (state: StoreState) => state[StoreKeys.Keys] -export const allKeys = createSelector(keysSlice, state => ({ - secretKeys: state.secretKeys, - userPublicKeys: state.userPublicKeys, - sigKeys: state.sigKeys, -})) - -export const keysSelectors = { - allKeys, -} +export const keysSelectors = {} diff --git a/packages/mobile/src/store/keys/keys.slice.ts b/packages/mobile/src/store/keys/keys.slice.ts index c69ef0ec9b..884b147bcf 100644 --- a/packages/mobile/src/store/keys/keys.slice.ts +++ b/packages/mobile/src/store/keys/keys.slice.ts @@ -1,25 +1,16 @@ import { createSlice, type PayloadAction } from '@reduxjs/toolkit' import { StoreKeys } from '../store.keys' -import { KeysUpdatedEvent, KeyWithMetadata } from '@quiet/types' +import { KeysUpdatedEvent } from '@quiet/types' import { createLogger } from '../../utils/logger' const logger = createLogger('keysSlice') -export class KeysState { - public secretKeys: KeyWithMetadata[] = [] - public userPublicKeys: KeyWithMetadata[] = [] - public sigKeys: KeyWithMetadata[] = [] -} +export class KeysState {} export const keysSlice = createSlice({ initialState: { ...new KeysState() }, name: StoreKeys.Keys, reducers: { - setKeys: (state, action: PayloadAction) => { - state.secretKeys = action.payload.secretKeys - state.sigKeys = action.payload.sigKeys - state.userPublicKeys = action.payload.userPublicKeys - }, saveKeysInKeychain: (state, _action: PayloadAction) => state, }, }) diff --git a/packages/mobile/src/store/keys/saveKeysInKeychain/saveKeysInKeychain.saga.ts b/packages/mobile/src/store/keys/saveKeysInKeychain/saveKeysInKeychain.saga.ts index 3e34095ef3..ae94020c36 100644 --- a/packages/mobile/src/store/keys/saveKeysInKeychain/saveKeysInKeychain.saga.ts +++ b/packages/mobile/src/store/keys/saveKeysInKeychain/saveKeysInKeychain.saga.ts @@ -2,8 +2,6 @@ import { type PayloadAction } from '@reduxjs/toolkit' import { call, select, put } from 'typed-redux-saga' import { KeysUpdatedEvent } from '@quiet/types' import { createLogger } from '../../../utils/logger' -import { keysActions } from '../keys.slice' -import { keysSelectors } from '../keys.selectors.' import _ from 'lodash' import { NativeModules } from 'react-native' @@ -12,48 +10,11 @@ import { StorableKey } from '../keys.type' const logger = createLogger('saveKeysInKeychainSaga') export function* saveKeysInKeychainSaga(action: PayloadAction): Generator { - logger.info('Storing keys in ios keychain') - const existingKeys = yield* select(keysSelectors.allKeys) - const newSecretKeys = _.differenceBy(action.payload.secretKeys, existingKeys.secretKeys, 'key') - const newUserPublicKeys = _.differenceBy(action.payload.userPublicKeys, existingKeys.userPublicKeys, 'key') - const newSigKeys = _.differenceBy(action.payload.sigKeys, existingKeys.sigKeys, 'key') - logger.info('Updating keys state') - yield* put(keysActions.setKeys(action.payload)) - - const keysToSave: StorableKey[] = newSecretKeys.map(keyWithMetadata => ({ - scope: { - ...keyWithMetadata.scope, - keyType: 'secret', - }, - key: keyWithMetadata.key, - teamId: action.payload.teamId, - })) - keysToSave.push( - ...newUserPublicKeys.map(keyWithMetadata => ({ - scope: { - ...keyWithMetadata.scope, - keyType: 'userPublic', - }, - key: keyWithMetadata.key, - teamId: action.payload.teamId, - })) - ) - keysToSave.push( - ...newSigKeys.map(keyWithMetadata => ({ - scope: { - ...keyWithMetadata.scope, - keyType: 'userSig', - }, - key: keyWithMetadata.key, - teamId: action.payload.teamId, - })) - ) - - logger.info('Putting new keys in keychain', keysToSave) + logger.info('Storing keys in ios keychain', action.payload.keys) try { yield* call( NativeModules.CommunicationModule.saveKeysInKeychain, - keysToSave.map(key => JSON.stringify(key)) + action.payload.keys.map(key => JSON.stringify(key)) ) } catch (e) { logger.error('Error while updating keys on keychain', e) diff --git a/packages/types/src/keys.ts b/packages/types/src/keys.ts index c4e78fcc98..7939aac759 100644 --- a/packages/types/src/keys.ts +++ b/packages/types/src/keys.ts @@ -1,13 +1,10 @@ import { Base58, KeyMetadata } from '@localfirst/crdx' -export interface KeyWithMetadata { - scope: KeyMetadata +export interface StorableKey { + keyName: string key: string | Base58 } export interface KeysUpdatedEvent { - secretKeys: KeyWithMetadata[] - userPublicKeys: KeyWithMetadata[] - sigKeys: KeyWithMetadata[] - teamId: string + keys: StorableKey[] } From 12d20d2e6623c992689087b442e6d0a243f57b99 Mon Sep 17 00:00:00 2001 From: Isla <5048549+islathehut@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:51:42 -0500 Subject: [PATCH 05/17] Add comments --- .../backend/src/nest/auth/sigchain.service.ts | 18 ++++++++++++++++-- .../src/nest/local-db/local-db.service.ts | 12 ++++++++++++ .../saveKeysInKeychain.saga.ts | 8 +++----- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/nest/auth/sigchain.service.ts b/packages/backend/src/nest/auth/sigchain.service.ts index 7870aa3d67..bed41eff38 100644 --- a/packages/backend/src/nest/auth/sigchain.service.ts +++ b/packages/backend/src/nest/auth/sigchain.service.ts @@ -151,6 +151,9 @@ export class SigChainService extends EventEmitter { this.logger.info('Chain updated, emitted updated event') } + /** + * Send updated list of users to the state manager on chain update + */ private _updateUsersOnChainUpdate() { const users = this.getActiveChain() .team?.members() @@ -163,8 +166,10 @@ export class SigChainService extends EventEmitter { this.serverIoProvider.io.emit(SocketEvents.USERS_UPDATED, { users }) } - // TODO: only fetch keys that have been updated recently - private async _updateKeysOnChainUpdate() { + /** + * Update the IOS keychain with any new keys on chain update + */ + private async _updateKeysOnChainUpdate(): Promise { if ((process.platform as string) !== 'ios') { this.logger.trace('Skipping key update because we are not on ios, current platform =', process.platform) return @@ -178,6 +183,7 @@ export class SigChainService extends EventEmitter { 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 = this.getActiveChain().crypto.getAllKeys() for (const keyData of Object.values(allKeys)) { for (const keyTypeData of Object.values(keyData)) { @@ -195,6 +201,7 @@ export class SigChainService extends EventEmitter { } } // 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 = this.getActiveChain().crypto.getPublicKeysForAllMembers(true) for (const keySet of allUserPublicKeys) { const publicKeyName = generateKeyName(teamId, StoredKeyType.USER_PUBLIC, { @@ -217,6 +224,13 @@ export class SigChainService extends EventEmitter { 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 local DB const keyUpdateEvent: KeysUpdatedEvent = { keys: keysToSend, } 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 040c0af71d..d1209bcb12 100644 --- a/packages/backend/src/nest/local-db/local-db.service.ts +++ b/packages/backend/src/nest/local-db/local-db.service.ts @@ -606,6 +606,12 @@ 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)) || [] @@ -613,6 +619,12 @@ export class LocalDbService extends EventEmitter { 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)) || [] diff --git a/packages/mobile/src/store/keys/saveKeysInKeychain/saveKeysInKeychain.saga.ts b/packages/mobile/src/store/keys/saveKeysInKeychain/saveKeysInKeychain.saga.ts index ae94020c36..45c6465713 100644 --- a/packages/mobile/src/store/keys/saveKeysInKeychain/saveKeysInKeychain.saga.ts +++ b/packages/mobile/src/store/keys/saveKeysInKeychain/saveKeysInKeychain.saga.ts @@ -1,12 +1,10 @@ import { type PayloadAction } from '@reduxjs/toolkit' -import { call, select, put } from 'typed-redux-saga' +import { call } from 'typed-redux-saga' +import { NativeModules } from 'react-native' + import { KeysUpdatedEvent } from '@quiet/types' import { createLogger } from '../../../utils/logger' -import _ from 'lodash' -import { NativeModules } from 'react-native' -import { StorableKey } from '../keys.type' - const logger = createLogger('saveKeysInKeychainSaga') export function* saveKeysInKeychainSaga(action: PayloadAction): Generator { From c4a2a23faa2b025ae8faf1888c29ec93d7c4a974 Mon Sep 17 00:00:00 2001 From: Isla <5048549+islathehut@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:53:26 -0500 Subject: [PATCH 06/17] Update CommunicationModule.swift --- packages/mobile/ios/CommunicationModule.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mobile/ios/CommunicationModule.swift b/packages/mobile/ios/CommunicationModule.swift index daa596255b..09c7b62330 100644 --- a/packages/mobile/ios/CommunicationModule.swift +++ b/packages/mobile/ios/CommunicationModule.swift @@ -72,6 +72,7 @@ class CommunicationModule: RCTEventEmitter { 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)") } } From 62978afaa735e2f851af5b1e382d8b21a1fc3b9a Mon Sep 17 00:00:00 2001 From: Isla <5048549+islathehut@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:59:04 -0500 Subject: [PATCH 07/17] Update KeychainHandler.swift --- packages/mobile/ios/KeychainHandler.swift | 52 +---------------------- 1 file changed, 1 insertion(+), 51 deletions(-) diff --git a/packages/mobile/ios/KeychainHandler.swift b/packages/mobile/ios/KeychainHandler.swift index 3b86f1e5e9..d478d84948 100644 --- a/packages/mobile/ios/KeychainHandler.swift +++ b/packages/mobile/ios/KeychainHandler.swift @@ -38,29 +38,13 @@ public struct NamedKey: Codable { let key: String } +// TODO: add string to key object conversion (e.g. string to SymmetricKey) @objc(KeychainHandler) class KeychainHandler: NSObject { - private let masterKeyName: String = "quiet_master_key" private let keychainGroupName: String = "com.quietmobile" private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "KeychainHandler") - public func getMasterKey() throws -> SymmetricKey { - do { - let password: String = try _getKeyImpl(keyName: masterKeyName) - let passwordBytes: ContiguousBytes = try _stringToBytes(str: password) - return SymmetricKey(data: passwordBytes) - } 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 getLfaKeyString(keyName: String) throws -> String { do { let password: String = try _getKeyImpl(keyName: keyName) @@ -76,28 +60,6 @@ class KeychainHandler: NSObject { } } - public func createMasterKey() throws -> SymmetricKey { - var existingKey: SymmetricKey? - do { - existingKey = try getMasterKey() - } catch KeychainHandlerError.noKeyFound { - existingKey = nil - } catch { - throw error - } - - guard existingKey == nil else { return existingKey! } - do { - let newKey: SymmetricKey = _generateAESKey() - let keyData: Data = _symmetricKeyToData(key: newKey) - let addStatus: KeyAddStatus = try _addKeyToKeychainImpl(keyName: masterKeyName, keyData: keyData) - guard addStatus == KeyAddStatus.success else { throw KeychainHandlerError.unhandledError(reason: addStatus) } - return newKey - } catch { - throw KeychainHandlerError.unhandledError(reason: error) - } - } - public func addLfaKey(namedKey: NamedKey) throws -> KeyAddStatus { var existingKey: String? do { @@ -166,21 +128,9 @@ class KeychainHandler: NSObject { } } - private func _generateAESKey() -> SymmetricKey { - let key: SymmetricKey = SymmetricKey(size: .bits256) - return key - } - private func _stringToBytes(str: String) throws -> Data { let bytes: Data? = str.data(using: .utf8) guard bytes != nil else { throw ConversionError.stringToBytesError } return bytes! } - - private func _symmetricKeyToData(key: SymmetricKey) -> Data { - let keyData: Data = key.withUnsafeBytes { body in - Data(body) - } - return keyData - } } From 10f5549956156280b7c1452e398cad86f4fab418 Mon Sep 17 00:00:00 2001 From: Isla <5048549+islathehut@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:00:32 -0500 Subject: [PATCH 08/17] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b875a72d32..9c85856f40 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 From dcc4bfcb86cbd0db17a5a7afd75394d6d8fd1304 Mon Sep 17 00:00:00 2001 From: Isla <5048549+islathehut@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:42:23 -0500 Subject: [PATCH 09/17] Pass actual team name since updates can happen when creating a chain before actually setting it active --- .../backend/src/nest/auth/sigchain.service.ts | 47 +++++++++++-------- packages/backend/src/nest/qss/qss.service.ts | 8 ++-- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/packages/backend/src/nest/auth/sigchain.service.ts b/packages/backend/src/nest/auth/sigchain.service.ts index bed41eff38..cdaacd35ee 100644 --- a/packages/backend/src/nest/auth/sigchain.service.ts +++ b/packages/backend/src/nest/auth/sigchain.service.ts @@ -14,7 +14,6 @@ import { 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, StorableKey, User } from '@quiet/types' import { type RoleService } from './services/roles/role.service' import { type DeviceService } from './services/members/device.service' @@ -33,12 +32,10 @@ export class SigChainService extends EventEmitter { private readonly logger = createLogger(SigChainService.name) private chains: Map = new Map() public connections: Map = new Map() - private lastUpdatedLink: Hash constructor( @Inject(SERVER_IO_PROVIDER) public readonly serverIoProvider: ServerIoProviderTypes, - private readonly localDbService: LocalDbService, - private readonly socketService: SocketService + private readonly localDbService: LocalDbService ) { super() } @@ -143,19 +140,19 @@ export class SigChainService extends EventEmitter { this.attachSocketListeners(this.getChain({ teamName })) } - private handleChainUpdate = () => { - this._updateUsersOnChainUpdate() - this._updateKeysOnChainUpdate() - this.emit('updated') - this.saveChain(this.activeChainTeamName!) + 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() { - const users = this.getActiveChain() + private _updateUsersOnChainUpdate(teamName: string) { + const users = this.getChain({ teamName }) .team?.members() .map(user => ({ userId: user.userId, @@ -169,7 +166,7 @@ export class SigChainService extends EventEmitter { /** * Update the IOS keychain with any new keys on chain update */ - private async _updateKeysOnChainUpdate(): Promise { + 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 @@ -179,12 +176,18 @@ export class SigChainService extends EventEmitter { return `quiet_${teamId}_${scope.type}_${scope.name}_${scope.generation}_${keyType}` } - const teamId = this.activeChain.team!.id + 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 = this.getActiveChain().crypto.getAllKeys() + 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)) { @@ -202,7 +205,7 @@ export class SigChainService extends EventEmitter { } // 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 = this.getActiveChain().crypto.getPublicKeysForAllMembers(true) + const allUserPublicKeys = sigchain.crypto.getPublicKeysForAllMembers(true) for (const keySet of allUserPublicKeys) { const publicKeyName = generateKeyName(teamId, StoredKeyType.USER_PUBLIC, { name: keySet.name, @@ -230,7 +233,7 @@ export class SigChainService extends EventEmitter { return } - // send new keys to the state manager to add to the keychain and update list of key names in local DB + // send new keys to the state manager to add to the keychain and update list of key names in const keyUpdateEvent: KeysUpdatedEvent = { keys: keysToSend, } @@ -240,12 +243,18 @@ export class SigChainService extends EventEmitter { 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) } /** @@ -299,7 +308,7 @@ export class SigChainService extends EventEmitter { const sigChain = SigChain.create(teamName, username) this.addChain(sigChain, setActive, teamName) await this.saveChain(teamName) - this.handleChainUpdate() + this.handleChainUpdate(teamName) return sigChain } 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) } } From 406ef6b5b5757bd3de0ed5aa20f7bd8d895d9b30 Mon Sep 17 00:00:00 2001 From: Isla <5048549+islathehut@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:23:18 -0400 Subject: [PATCH 10/17] Move update user profiles to a separate saga --- .../userProfile/updateUserProfiles.saga.ts | 52 +++++++++++++++++++ .../src/sagas/users/users.master.saga.ts | 2 + .../src/sagas/users/users.slice.ts | 41 +-------------- 3 files changed, 55 insertions(+), 40 deletions(-) create mode 100644 packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.ts diff --git a/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.ts b/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.ts new file mode 100644 index 0000000000..0b8cc40197 --- /dev/null +++ b/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.ts @@ -0,0 +1,52 @@ +import { PayloadAction } from '@reduxjs/toolkit' +import { createLogger } from '../../../utils/logger' +import { put, select } from 'typed-redux-saga' +import { userProfileSelectors } from './userProfile.selectors' +import { UserProfile } from '@quiet/types' +import { Socket } from '../../../types' +import { usersActions } from '../users.slice' + +const logger = createLogger('updateUserProfilesSaga') + +export function* updateUserProfilesSaga(socket: Socket, action: PayloadAction): Generator { + logger.info(`Updating user profiles (profile count = ${action.payload.length}`) + const userProfiles = yield* select(userProfileSelectors.userProfiles) + const updates = { ...userProfiles } + for (const userProfile of action.payload) { + if (updates[userProfile.userId]) { + const existingProfile = updates[userProfile.userId] + + const updatedProfile = { + ...existingProfile, + ...userProfile, + } + + // If CID is the same, preserve the existing path + if ( + userProfile.profilePhoto?.cid && + existingProfile.profilePhoto?.cid === userProfile.profilePhoto.cid && + existingProfile.profilePhoto?.path + ) { + updatedProfile.profilePhoto = { + ...userProfile.profilePhoto, + path: existingProfile.profilePhoto.path, + } + } + + // If CID changed, ensure path is null (it should be null from userProfile anyway, but let's be explicit) + if (userProfile.profilePhoto?.cid && existingProfile.profilePhoto?.cid !== userProfile.profilePhoto.cid) { + updatedProfile.profilePhoto = { + ...userProfile.profilePhoto, + path: null, + } + } + + updates[userProfile.userId] = updatedProfile + } else { + updates[userProfile.userId] = userProfile + } + } + logger.debug(`Updating user profiles in redux store`) + yield* put(usersActions.setUserProfiles(Object.values(updates))) + logger.debug(`Done`) +} diff --git a/packages/state-manager/src/sagas/users/users.master.saga.ts b/packages/state-manager/src/sagas/users/users.master.saga.ts index 533dde98e1..cbbe6e0639 100644 --- a/packages/state-manager/src/sagas/users/users.master.saga.ts +++ b/packages/state-manager/src/sagas/users/users.master.saga.ts @@ -5,6 +5,7 @@ import { usersActions } from './users.slice' import { saveUserProfileSaga } from './userProfile/saveUserProfile.saga' import { downloadProfilePhotosSaga } from './userProfile/downloadProfilePhotos.saga' import { createLogger } from '../../utils/logger' +import { updateUserProfilesSaga } from './userProfile/updateUserProfiles.saga' const logger = createLogger('usersMasterSaga') @@ -13,6 +14,7 @@ export function* usersMasterSaga(socket: Socket): Generator { try { yield all([ takeEvery(usersActions.saveUserProfile.type, saveUserProfileSaga, socket), + takeEvery(usersActions.updateUserProfiles.type, updateUserProfilesSaga, socket), takeEvery(usersActions.updateUserProfiles.type, downloadProfilePhotosSaga), ]) } finally { diff --git a/packages/state-manager/src/sagas/users/users.slice.ts b/packages/state-manager/src/sagas/users/users.slice.ts index d253d51f37..54064031b7 100644 --- a/packages/state-manager/src/sagas/users/users.slice.ts +++ b/packages/state-manager/src/sagas/users/users.slice.ts @@ -40,46 +40,7 @@ export const usersSlice = createSlice({ } return state }, - updateUserProfiles: (state, action: PayloadAction) => { - if (!state.userProfiles) { - state.userProfiles = {} - } - for (const userProfile of action.payload) { - if (state.userProfiles[userProfile.userId]) { - const existingProfile = state.userProfiles[userProfile.userId] - - const updatedProfile = { - ...existingProfile, - ...userProfile, - } - - // If CID is the same, preserve the existing path - if ( - userProfile.profilePhoto?.cid && - existingProfile.profilePhoto?.cid === userProfile.profilePhoto.cid && - existingProfile.profilePhoto?.path - ) { - updatedProfile.profilePhoto = { - ...userProfile.profilePhoto, - path: existingProfile.profilePhoto.path, - } - } - - // If CID changed, ensure path is null (it should be null from userProfile anyway, but let's be explicit) - if (userProfile.profilePhoto?.cid && existingProfile.profilePhoto?.cid !== userProfile.profilePhoto.cid) { - updatedProfile.profilePhoto = { - ...userProfile.profilePhoto, - path: null, - } - } - - state.userProfiles[userProfile.userId] = updatedProfile - } else { - state.userProfiles[userProfile.userId] = userProfile - } - } - return state - }, + updateUserProfiles: (state, _action: PayloadAction) => state, // Sets a single user profile, overwriting the existing one setUserProfile: (state, action: PayloadAction) => { // Creating user profiles object for backwards compatibility with 2.0.1 From f9391fcb98ead84a0c75f2982106b37beebbcb34 Mon Sep 17 00:00:00 2001 From: Isla <5048549+islathehut@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:30:01 -0400 Subject: [PATCH 11/17] Pass updated user profiles to native ios code --- .../connections-manager.service.ts | 6 +++++ .../backend/src/nest/socket/socket.service.ts | 6 +++++ packages/mobile/ios/CommunicationBridge.m | 1 + packages/mobile/ios/CommunicationModule.swift | 24 +++++++++++++++++++ .../ios/Quiet.xcodeproj/project.pbxproj | 4 ++++ packages/mobile/ios/UserMetadataHandler.swift | 24 +++++++++++++++++++ .../startConnection/startConnection.saga.ts | 8 ++++++- packages/mobile/src/store/root.reducer.ts | 2 ++ packages/mobile/src/store/root.saga.ts | 4 +++- packages/mobile/src/store/store.keys.ts | 1 + .../saveUserMetadataNatively.saga.ts | 18 ++++++++++++++ .../userMetadata/usersMetadata.master.saga.ts | 19 +++++++++++++++ .../userMetadata/usersMetadata.selectors..ts | 7 ++++++ .../store/userMetadata/usersMetadata.slice.ts | 19 +++++++++++++++ .../userMetadata/usersMetadata.transform.ts | 14 +++++++++++ .../store/userMetadata/usersMetadata.types.ts | 12 ++++++++++ .../startConnection/startConnection.saga.ts | 1 + .../userProfile/updateUserProfiles.saga.ts | 23 ++++++++++++------ packages/types/src/socket.ts | 5 ++++ packages/types/src/user.ts | 4 ++++ 20 files changed, 193 insertions(+), 9 deletions(-) create mode 100644 packages/mobile/ios/UserMetadataHandler.swift create mode 100644 packages/mobile/src/store/userMetadata/saveUserMetadataNatively/saveUserMetadataNatively.saga.ts create mode 100644 packages/mobile/src/store/userMetadata/usersMetadata.master.saga.ts create mode 100644 packages/mobile/src/store/userMetadata/usersMetadata.selectors..ts create mode 100644 packages/mobile/src/store/userMetadata/usersMetadata.slice.ts create mode 100644 packages/mobile/src/store/userMetadata/usersMetadata.transform.ts create mode 100644 packages/mobile/src/store/userMetadata/usersMetadata.types.ts diff --git a/packages/backend/src/nest/connections-manager/connections-manager.service.ts b/packages/backend/src/nest/connections-manager/connections-manager.service.ts index ff03f1a5ec..45aa25c4fc 100644 --- a/packages/backend/src/nest/connections-manager/connections-manager.service.ts +++ b/packages/backend/src/nest/connections-manager/connections-manager.service.ts @@ -50,6 +50,7 @@ import { SetUserProfilePayload, InvitationData, SetUserProfileResponse, + UserProfilesUpdatedPayload, } from '@quiet/types' import { CONFIG_OPTIONS, QSS_ALLOWED, QSS_ENDPOINT, SERVER_IO_PROVIDER, SOCKS_PROXY_AGENT } from '../const' import { Libp2pService, Libp2pState } from '../libp2p/libp2p.service' @@ -809,6 +810,11 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI } ) + this.socketService.on(SocketActions.USER_PROFILES_UPDATED, (payload: UserProfilesUpdatedPayload) => { + this.logger.info(`Forwarding ${SocketActions.USER_PROFILES_UPDATED} back to state manager`) + this.serverIoProvider.io.emit(SocketEvents.USER_PROFILES_UPDATED, payload) + }) + this.socketService.on(SocketActions.TOGGLE_P2P, async (payload: boolean, callback: (response: boolean) => void) => { try { if (payload) { diff --git a/packages/backend/src/nest/socket/socket.service.ts b/packages/backend/src/nest/socket/socket.service.ts index 18506f70d0..aefd7aeaa8 100644 --- a/packages/backend/src/nest/socket/socket.service.ts +++ b/packages/backend/src/nest/socket/socket.service.ts @@ -24,6 +24,7 @@ import { SetUserProfilePayload, type HCaptchaFormResponse, InviteResultWithSalt, + UserProfilesUpdatedPayload, } from '@quiet/types' import EventEmitter from 'events' import { CONFIG_OPTIONS, SERVER_IO_PROVIDER } from '../const' @@ -199,6 +200,11 @@ export class SocketService extends EventEmitter implements OnModuleInit { } ) + socket.on(SocketActions.USER_PROFILES_UPDATED, (payload: UserProfilesUpdatedPayload) => { + this.logger.info(`Emitting ${SocketActions.USER_PROFILES_UPDATED}`) + this.emit(SocketActions.USER_PROFILES_UPDATED, payload) + }) + // ====== Local First Auth ====== socket.on( diff --git a/packages/mobile/ios/CommunicationBridge.m b/packages/mobile/ios/CommunicationBridge.m index 5f0e6155ef..9212ba937e 100644 --- a/packages/mobile/ios/CommunicationBridge.m +++ b/packages/mobile/ios/CommunicationBridge.m @@ -6,4 +6,5 @@ @interface RCT_EXTERN_MODULE(CommunicationModule, RCTEventEmitter) RCT_EXTERN_METHOD(requestNotificationPermission) RCT_EXTERN_METHOD(checkNotificationPermission) RCT_EXTERN_METHOD(saveKeysInKeychain:(NSArray *)newKeys) +RCT_EXTERN_METHOD(saveUserMetadata:(NSArray *)updatedMetadata) @end diff --git a/packages/mobile/ios/CommunicationModule.swift b/packages/mobile/ios/CommunicationModule.swift index 09c7b62330..6614ceddcc 100644 --- a/packages/mobile/ios/CommunicationModule.swift +++ b/packages/mobile/ios/CommunicationModule.swift @@ -16,6 +16,7 @@ class CommunicationModule: RCTEventEmitter { private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "CommunicationModule") let keychainHandler = KeychainHandler() + let userMetadataHandler = UserMetadataHandler() @objc func sendDataPort(port: UInt16, socketIOSecret: String) { @@ -78,6 +79,29 @@ class CommunicationModule: RCTEventEmitter { } } + @objc + func saveUserMetadata(_ updatedMetadata: NSArray) { + let decoder = JSONDecoder() + var userMetadata: [UserMetadata] = [] + for metadataAsAny in updatedMetadata { + do { + let metadataAsString: String = metadataAsAny as! String + let data = Data(metadataAsString.utf8) + let decodedMetadata = try decoder.decode(UserMetadata.self, from: data) + CommunicationModule.logger.info("Decoded user metadata: \(String(describing: decodedMetadata))") + userMetadata.append(decodedMetadata) + } catch { + CommunicationModule.logger.error("Error while decoding user metadata: \(error)") + } + } + + do { + try self.userMetadataHandler.saveUserMetadata(updatedMetadata: userMetadata) + } catch { + CommunicationModule.logger.error("Error while saving user metadata: \(error)") + } + } + @objc func checkNotificationPermission() { UNUserNotificationCenter.current().getNotificationSettings { settings in diff --git a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj index e1d226dc60..9260b8638c 100644 --- a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj @@ -55,6 +55,7 @@ 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 */; }; + 663DC8C12F621139005D2086 /* UserMetadataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663DC8C02F621134005D2086 /* UserMetadataHandler.swift */; }; 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 */; }; @@ -620,6 +621,7 @@ 18FD2A3B296F009E00A2B8C0 /* QuietDebug.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; name = QuietDebug.entitlements; path = Quiet/QuietDebug.entitlements; 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 = ""; }; + 663DC8C02F621134005D2086 /* UserMetadataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMetadataHandler.swift; 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 = ""; }; @@ -672,6 +674,7 @@ 13B07FAE1A68108700A75B9A /* Quiet */ = { isa = PBXGroup; children = ( + 663DC8C02F621134005D2086 /* UserMetadataHandler.swift */, 665587C92F4F5ECD005D2086 /* KeychainHandler.swift */, 180E120A2AEFB7F900804659 /* Utils.swift */, 18FD2A36296F009E00A2B8C0 /* AppDelegate.h */, @@ -5174,6 +5177,7 @@ 955DC7582BD930B30014725B /* WebsocketSingleton.swift in Sources */, 1889CA4E26E763E1004ECFBD /* Extensions.swift in Sources */, 1898143A2934CF70001F39E7 /* TorHandler.swift in Sources */, + 663DC8C12F621139005D2086 /* UserMetadataHandler.swift in Sources */, 1868BCEE292E9212001D6D5E /* RNNodeJsMobile.m in Sources */, 1868C4382930D7D6001D6D5E /* DataDirectory.swift in Sources */, 1868C43E2930EAEA001D6D5E /* CommunicationBridge.m in Sources */, diff --git a/packages/mobile/ios/UserMetadataHandler.swift b/packages/mobile/ios/UserMetadataHandler.swift new file mode 100644 index 0000000000..6e15a410c2 --- /dev/null +++ b/packages/mobile/ios/UserMetadataHandler.swift @@ -0,0 +1,24 @@ +// +// UserMetadataHandler.swift +// Quiet +// +// Created by Isla Koenigsknecht on 2/25/26. +// + +import CoreData +import OSLog + +public struct UserMetadata: Codable { + let userId: String + let nickname: String + let photo: String? +} + +@objc(UserMetadataHandler) +class UserMetadataHandler: NSObject { + private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "UserMetadataHandler") + + public func saveUserMetadata(updatedMetadata: [UserMetadata]) throws -> Void { + UserMetadataHandler.logger.info("Storing user metadata") + } +} diff --git a/packages/mobile/src/store/init/startConnection/startConnection.saga.ts b/packages/mobile/src/store/init/startConnection/startConnection.saga.ts index 6a83e68746..4c12d91f62 100644 --- a/packages/mobile/src/store/init/startConnection/startConnection.saga.ts +++ b/packages/mobile/src/store/init/startConnection/startConnection.saga.ts @@ -16,10 +16,11 @@ 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 { KeysUpdatedEvent, SocketActions, SocketEvents } from '@quiet/types' +import { KeysUpdatedEvent, SocketActions, SocketEvents, UserProfilesUpdatedPayload } from '@quiet/types' import { createLogger } from '../../../utils/logger' import { initSelectors } from '../init.selectors' import { keysActions } from '../../keys/keys.slice' +import { usersMetadataActions } from '../../userMetadata/usersMetadata.slice' const logger = createLogger('startConnection') @@ -80,6 +81,7 @@ function subscribeSocketLifecycle(socket: Socket, socketIOData: WebsocketConnect | ReturnType | ReturnType | ReturnType + | ReturnType >(emit => { socket.on('connect', async () => { socket_id = socket.id @@ -94,6 +96,10 @@ function subscribeSocketLifecycle(socket: Socket, socketIOData: WebsocketConnect logger.info('Keys updated, writing to keychain') emit(keysActions.saveKeysInKeychain(payload)) }) + socket.on(SocketEvents.USER_PROFILES_UPDATED, async (payload: UserProfilesUpdatedPayload) => { + logger.info('User profiles updated, saving in ios native storage') + emit(usersMetadataActions.saveUserMetadataNatively(payload)) + }) return () => {} }) } diff --git a/packages/mobile/src/store/root.reducer.ts b/packages/mobile/src/store/root.reducer.ts index 0ffbbbb304..bfc6037d4d 100644 --- a/packages/mobile/src/store/root.reducer.ts +++ b/packages/mobile/src/store/root.reducer.ts @@ -6,6 +6,7 @@ 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' +import { usersMetadataReducer } from './userMetadata/usersMetadata.slice' export const reducers = { ...stateManagerReducers.reducers, @@ -14,6 +15,7 @@ export const reducers = { [StoreKeys.NativeServices]: nativeServicesReducer, [StoreKeys.PushNotifications]: pushNotificationsReducer, [StoreKeys.Keys]: keysReducer, + [StoreKeys.UsersMetadata]: usersMetadataReducer, } export const allReducers = combineReducers(reducers) diff --git a/packages/mobile/src/store/root.saga.ts b/packages/mobile/src/store/root.saga.ts index 98a149f6a6..5a818fbf6d 100644 --- a/packages/mobile/src/store/root.saga.ts +++ b/packages/mobile/src/store/root.saga.ts @@ -3,13 +3,14 @@ import { nativeServicesMasterSaga } from './nativeServices/nativeServices.master import { navigationMasterSaga } from './navigation/navigation.master.saga' import { initMasterSaga } from './init/init.master.saga' import { initActions } from './init/init.slice' -import { publicChannels } from '@quiet/state-manager' +import { publicChannels, Socket } from '@quiet/state-manager' import { showNotificationSaga } from './nativeServices/showNotification/showNotification.saga' import { clearReduxStore } from './nativeServices/leaveCommunity/leaveCommunity.saga' import { pushNotificationsMasterSaga } from './pushNotifications/pushNotifications.master.saga' import { setEngine, CryptoEngine } from 'pkijs' import { createLogger } from '../utils/logger' import { keysMasterSaga } from './keys/keys.master.saga' +import { usersMetadataMasterSaga } from './userMetadata/usersMetadata.master.saga' const logger = createLogger('root') @@ -56,6 +57,7 @@ function* storeReadySaga(): Generator { fork(nativeServicesMasterSaga), fork(pushNotificationsMasterSaga), fork(keysMasterSaga), + fork(usersMetadataMasterSaga), // 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 9389713392..d015f270f8 100644 --- a/packages/mobile/src/store/store.keys.ts +++ b/packages/mobile/src/store/store.keys.ts @@ -4,4 +4,5 @@ export enum StoreKeys { NativeServices = 'NativeServices', PushNotifications = 'PushNotifications', Keys = 'Keys', + UsersMetadata = 'UsersMetadata', } diff --git a/packages/mobile/src/store/userMetadata/saveUserMetadataNatively/saveUserMetadataNatively.saga.ts b/packages/mobile/src/store/userMetadata/saveUserMetadataNatively/saveUserMetadataNatively.saga.ts new file mode 100644 index 0000000000..577219221e --- /dev/null +++ b/packages/mobile/src/store/userMetadata/saveUserMetadataNatively/saveUserMetadataNatively.saga.ts @@ -0,0 +1,18 @@ +import { type PayloadAction } from '@reduxjs/toolkit' +import { call } from 'typed-redux-saga' +import { NativeModules } from 'react-native' + +import { UserProfilesUpdatedPayload } from '@quiet/types' +import { createLogger } from '../../../utils/logger' + +const logger = createLogger('saveUserMetadataNativelySaga') + +export function* saveUserMetadataNativelySaga(action: PayloadAction): Generator { + logger.info('Storing user metadata in ios native storage', action.payload.profiles) + try { + const updates: string[] = action.payload.profiles.map(profile => JSON.stringify(profile)) + yield* call(NativeModules.CommunicationModule.saveUserMetadata, updates) + } catch (e) { + logger.error('Error while updating user metadata in ios native storage', e) + } +} diff --git a/packages/mobile/src/store/userMetadata/usersMetadata.master.saga.ts b/packages/mobile/src/store/userMetadata/usersMetadata.master.saga.ts new file mode 100644 index 0000000000..1be76b3594 --- /dev/null +++ b/packages/mobile/src/store/userMetadata/usersMetadata.master.saga.ts @@ -0,0 +1,19 @@ +import { takeEvery, cancelled } from 'redux-saga/effects' +import { all } from 'typed-redux-saga' +import { saveUserMetadataNativelySaga } from './saveUserMetadataNatively/saveUserMetadataNatively.saga' +import { createLogger } from '../../utils/logger' +import { usersMetadataActions } from './usersMetadata.slice' + +const logger = createLogger('usersMetadataMasterSaga') + +export function* usersMetadataMasterSaga(): Generator { + logger.info('usersMetadataMasterSaga starting') + try { + yield all([takeEvery(usersMetadataActions.saveUserMetadataNatively.type, saveUserMetadataNativelySaga)]) + } finally { + logger.info('usersMetadataMasterSaga stopping') + if (yield cancelled()) { + logger.info('usersMetadataMasterSaga cancelled') + } + } +} diff --git a/packages/mobile/src/store/userMetadata/usersMetadata.selectors..ts b/packages/mobile/src/store/userMetadata/usersMetadata.selectors..ts new file mode 100644 index 0000000000..513827bdda --- /dev/null +++ b/packages/mobile/src/store/userMetadata/usersMetadata.selectors..ts @@ -0,0 +1,7 @@ +import { StoreKeys } from '../store.keys' +import { CreatedSelectors, StoreState } from '../store.types' + +const usersMetadataSlice: CreatedSelectors[StoreKeys.UsersMetadata] = (state: StoreState) => + state[StoreKeys.UsersMetadata] + +export const usersMetadataSelectors = {} diff --git a/packages/mobile/src/store/userMetadata/usersMetadata.slice.ts b/packages/mobile/src/store/userMetadata/usersMetadata.slice.ts new file mode 100644 index 0000000000..7c415e8c50 --- /dev/null +++ b/packages/mobile/src/store/userMetadata/usersMetadata.slice.ts @@ -0,0 +1,19 @@ +import { createSlice, type PayloadAction } from '@reduxjs/toolkit' +import { StoreKeys } from '../store.keys' +import { UserProfilesUpdatedPayload } from '@quiet/types' +import { createLogger } from '../../utils/logger' + +const logger = createLogger('keysSlice') + +export class UsersMetadataState {} + +export const usersMetadataSlice = createSlice({ + initialState: { ...new UsersMetadataState() }, + name: StoreKeys.Keys, + reducers: { + saveUserMetadataNatively: (state, _action: PayloadAction) => state, + }, +}) + +export const usersMetadataActions = usersMetadataSlice.actions +export const usersMetadataReducer = usersMetadataSlice.reducer diff --git a/packages/mobile/src/store/userMetadata/usersMetadata.transform.ts b/packages/mobile/src/store/userMetadata/usersMetadata.transform.ts new file mode 100644 index 0000000000..8bb98ffe53 --- /dev/null +++ b/packages/mobile/src/store/userMetadata/usersMetadata.transform.ts @@ -0,0 +1,14 @@ +import { createTransform } from 'redux-persist' +import { StoreKeys } from '../store.keys' +import { UsersMetadataState } from './usersMetadata.slice' + +export const UsersMetadataTransform = createTransform( + (inboundState: UsersMetadataState, _key: any) => { + return inboundState + }, + (outboundState: UsersMetadataState, _key: any) => { + // TODO: determine if we still need this transform + return outboundState + }, + { whitelist: [StoreKeys.UsersMetadata] } +) diff --git a/packages/mobile/src/store/userMetadata/usersMetadata.types.ts b/packages/mobile/src/store/userMetadata/usersMetadata.types.ts new file mode 100644 index 0000000000..5148b443a9 --- /dev/null +++ b/packages/mobile/src/store/userMetadata/usersMetadata.types.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/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts b/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts index 4215fdf693..e988254ddf 100644 --- a/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts +++ b/packages/state-manager/src/sagas/socket/startConnection/startConnection.saga.ts @@ -41,6 +41,7 @@ import { HCaptchaRequest, HCaptchaChallengeRequest, InviteResultWithSalt, + UserProfilesUpdatedPayload, } from '@quiet/types' import { createLogger } from '../../../utils/logger' diff --git a/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.ts b/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.ts index 0b8cc40197..b46f698d33 100644 --- a/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.ts +++ b/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.ts @@ -1,15 +1,15 @@ import { PayloadAction } from '@reduxjs/toolkit' import { createLogger } from '../../../utils/logger' -import { put, select } from 'typed-redux-saga' +import { apply, call, put, select } from 'typed-redux-saga' import { userProfileSelectors } from './userProfile.selectors' -import { UserProfile } from '@quiet/types' -import { Socket } from '../../../types' +import { SocketActions, SocketEvents, SocketEventsMap, UserProfile } from '@quiet/types' +import { applyEmitParams, Socket } from '../../../types' import { usersActions } from '../users.slice' const logger = createLogger('updateUserProfilesSaga') export function* updateUserProfilesSaga(socket: Socket, action: PayloadAction): Generator { - logger.info(`Updating user profiles (profile count = ${action.payload.length}`) + logger.info(`Updating user profiles (profile count = ${action.payload.length})`) const userProfiles = yield* select(userProfileSelectors.userProfiles) const updates = { ...userProfiles } for (const userProfile of action.payload) { @@ -46,7 +46,16 @@ export function* updateUserProfilesSaga(socket: Socket, action: PayloadAction void> [SocketActions.LOAD_MIGRATION_DATA]: EmitEvent> + [SocketActions.USER_PROFILES_UPDATED]: EmitEvent // ====== Local First Auth ====== [SocketActions.VALIDATE_OR_CREATE_LONG_LIVED_LFA_INVITE]: EmitEvent< @@ -238,6 +242,7 @@ export interface SocketEventsMap { [SocketEvents.USERS_REMOVED]: EmitEvent [SocketEvents.USER_PROFILES_STORED]: EmitEvent [SocketEvents.KEYS_UPDATED]: EmitEvent + [SocketEvents.USER_PROFILES_UPDATED]: EmitEvent // ====== Files ====== [SocketEvents.FILE_ATTACHED]: EmitEvent diff --git a/packages/types/src/user.ts b/packages/types/src/user.ts index 4319c7d3af..72197c39ac 100644 --- a/packages/types/src/user.ts +++ b/packages/types/src/user.ts @@ -59,6 +59,10 @@ export interface UserProfilesStoredEvent { profiles: UserProfile[] } +export interface UserProfilesUpdatedPayload { + profiles: UserProfile[] +} + export interface UsersUpdatedEvent { users: User[] } From 1497029d3223aba2fdec4875eb8721edcdc912a2 Mon Sep 17 00:00:00 2001 From: Isla <5048549+islathehut@users.noreply.github.com> Date: Fri, 13 Mar 2026 11:13:33 -0400 Subject: [PATCH 12/17] Persist user metadata in native storage --- packages/mobile/ios/CommunicationModule.swift | 13 +- packages/mobile/ios/KeychainHandler.swift | 2 +- packages/mobile/ios/UserMetadataHandler.swift | 173 +++++++++++++++++- .../saveUserMetadataNatively.saga.ts | 7 +- packages/state-manager/package-lock.json | 30 ++- packages/state-manager/package.json | 2 + .../userProfile/updateUserProfiles.saga.ts | 34 ++-- packages/types/src/user.ts | 3 +- 8 files changed, 233 insertions(+), 31 deletions(-) diff --git a/packages/mobile/ios/CommunicationModule.swift b/packages/mobile/ios/CommunicationModule.swift index 6614ceddcc..1c25606062 100644 --- a/packages/mobile/ios/CommunicationModule.swift +++ b/packages/mobile/ios/CommunicationModule.swift @@ -82,12 +82,12 @@ class CommunicationModule: RCTEventEmitter { @objc func saveUserMetadata(_ updatedMetadata: NSArray) { let decoder = JSONDecoder() - var userMetadata: [UserMetadata] = [] + var userMetadata: [UserMetadataStruct] = [] for metadataAsAny in updatedMetadata { do { let metadataAsString: String = metadataAsAny as! String let data = Data(metadataAsString.utf8) - let decodedMetadata = try decoder.decode(UserMetadata.self, from: data) + let decodedMetadata = try decoder.decode(UserMetadataStruct.self, from: data) CommunicationModule.logger.info("Decoded user metadata: \(String(describing: decodedMetadata))") userMetadata.append(decodedMetadata) } catch { @@ -100,6 +100,15 @@ class CommunicationModule: RCTEventEmitter { } catch { CommunicationModule.logger.error("Error while saving user metadata: \(error)") } + + for metadata in userMetadata { + do { + let stored = try self.userMetadataHandler.fetchUserMetadataById(userId: metadata.userId) + CommunicationModule.logger.info("Passed: \(String(describing: metadata)), Stored: \(String(describing: stored?.toStruct()))") + } catch { + // do nothing + } + } } @objc diff --git a/packages/mobile/ios/KeychainHandler.swift b/packages/mobile/ios/KeychainHandler.swift index d478d84948..355365f045 100644 --- a/packages/mobile/ios/KeychainHandler.swift +++ b/packages/mobile/ios/KeychainHandler.swift @@ -5,7 +5,7 @@ // Created by Isla Koenigsknecht on 2/25/26. // - +import Foundation import CryptoKit import Security import CoreData diff --git a/packages/mobile/ios/UserMetadataHandler.swift b/packages/mobile/ios/UserMetadataHandler.swift index 6e15a410c2..fad190915c 100644 --- a/packages/mobile/ios/UserMetadataHandler.swift +++ b/packages/mobile/ios/UserMetadataHandler.swift @@ -5,20 +5,185 @@ // Created by Isla Koenigsknecht on 2/25/26. // +import Foundation import CoreData import OSLog +import SwiftData -public struct UserMetadata: Codable { +public enum UserMetadataError: Error { + case missingModelContext + case noModelFound(id: String) + case incorrectModelCount(expected: Int, actual: Int) + case unhandledError(reason: Error) +} + +public struct ProfilePhotoStruct: Codable { + let ext: String + let path: String? + let size: Int + let width: Int + let height: Int +} + +public struct UserMetadataStruct: Codable { let userId: String let nickname: String - let photo: String? + let profilePhoto: ProfilePhotoStruct? +} + +@Model +class ProfilePhoto: Identifiable { + var id: String + var ext: String + var path: String? + var size: Int + var width: Int + var height: Int + var userMetadata: UserMetadata? + var createdAt: Date? + + init(id: String, ext: String, path: String?, size: Int, width: Int, height: Int, createdAt: Date?) { + self.id = id + self.ext = ext + self.path = path + self.size = size + self.width = width + self.height = height + self.createdAt = createdAt + } + + public func toStruct() -> ProfilePhotoStruct { + return ProfilePhotoStruct(ext: self.ext, path: self.path, size: self.size, width: self.width, height: self.height) + } +} + +@Model +class UserMetadata: Identifiable { + var id: String + var nickname: String + var createdAt: Date? = nil + + @Relationship(deleteRule: .cascade, inverse: \ProfilePhoto.userMetadata) + var profilePhoto: ProfilePhoto? + + init(id: String, nickname: String, profilePhoto: ProfilePhotoStruct?, createdAt: Date?) { + var profilePhotoModel: ProfilePhoto? = nil + if (profilePhoto != nil) { + profilePhotoModel = ProfilePhoto(id: id, ext: profilePhoto!.ext, path: profilePhoto!.path, size: profilePhoto!.size, width: profilePhoto!.width, height: profilePhoto!.height, createdAt: createdAt) + } + self.id = id + self.nickname = nickname + self.profilePhoto = profilePhotoModel + self.createdAt = createdAt + } + + public static func fromStruct(userMetadata: UserMetadataStruct, createdAt: Date?) -> UserMetadata { + return UserMetadata(id: userMetadata.userId, nickname: userMetadata.nickname, profilePhoto: userMetadata.profilePhoto, createdAt: createdAt) + } + + public func toStruct() -> UserMetadataStruct { + return UserMetadataStruct( + userId: self.id, + nickname: self.nickname, + profilePhoto: self.profilePhoto?.toStruct() + ) + } } @objc(UserMetadataHandler) class UserMetadataHandler: NSObject { private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "UserMetadataHandler") + private var container: ModelContainer? + private var modelContext: ModelContext? - public func saveUserMetadata(updatedMetadata: [UserMetadata]) throws -> Void { - UserMetadataHandler.logger.info("Storing user metadata") + public func initContainer() throws -> Void { + if (self.container != nil) { + UserMetadataHandler.logger.debug("Container already initialized, skipping...") + return + } + + do { + self.container = try ModelContainer(for: UserMetadata.self, configurations: .init(isStoredInMemoryOnly: false)) + self.modelContext = ModelContext(self.container!) + } catch { + UserMetadataHandler.logger.error("Error while initializing UserMetadata ModelContainer: \(error)") + throw error + } + } + + public func saveUserMetadata(updatedMetadata: [UserMetadataStruct]) throws -> Void { + do { + try self.initContainer() + } catch { + throw error + } + + guard let context = self.modelContext else { + throw UserMetadataError.missingModelContext + } + + UserMetadataHandler.logger.info("Inserting user metadata") + for metadata in updatedMetadata { + UserMetadataHandler.logger.debug("Inserting data: \(String(describing: metadata))") + let model = UserMetadata.fromStruct(userMetadata: metadata, createdAt: Date.now) + let found = try self.fetchUserMetadataById(userId: model.id) + if (found != nil) { + UserMetadataHandler.logger.debug("Replacing existing metadata for \(model.id)") + try self.deleteUserMetadata(model: model) + } + context.insert(model) + } + + UserMetadataHandler.logger.info("Persisting user metadata") + do { + try context.save() + } catch { + UserMetadataHandler.logger.error("Error while persisting UserMetadata model(s) to disk: \(error)") + throw KeychainHandlerError.unhandledError(reason: error) + } + } + + public func fetchUserMetadataById(userId: String) throws -> UserMetadata? { + UserMetadataHandler.logger.info("Fetching UserMetadata by ID: \(userId)") + + guard let context = self.modelContext else { + throw UserMetadataError.missingModelContext + } + + do { + let descriptor = FetchDescriptor( + predicate: #Predicate { $0.id == userId } + ) + let models = try context.fetch(descriptor) + guard models.count > 0 else { + return nil + } + guard models.count == 1 else { + for model in models { + UserMetadataHandler.logger.warning("Found: \(String(describing: model.toStruct()))") + } + throw UserMetadataError.incorrectModelCount(expected: 1, actual: models.count) + } + return models[0] + } catch { + UserMetadataHandler.logger.error("Error while fetching UserMetadata for ID \(userId): \(error)") + throw UserMetadataError.unhandledError(reason: error) + } + } + + public func deleteUserMetadata(model: UserMetadata) throws -> Void { + UserMetadataHandler.logger.info("Deleting UserMetadata record: \(String(describing: model.toStruct()))") + + guard let context = self.modelContext else { + throw UserMetadataError.missingModelContext + } + + do { + context.delete(model) + try context.save() + } catch { + UserMetadataHandler.logger.error("Error while deleting UserMetadata \(String(describing: model.toStruct())): \(error)") + throw UserMetadataError.unhandledError(reason: error) + } } } diff --git a/packages/mobile/src/store/userMetadata/saveUserMetadataNatively/saveUserMetadataNatively.saga.ts b/packages/mobile/src/store/userMetadata/saveUserMetadataNatively/saveUserMetadataNatively.saga.ts index 577219221e..dba157954a 100644 --- a/packages/mobile/src/store/userMetadata/saveUserMetadataNatively/saveUserMetadataNatively.saga.ts +++ b/packages/mobile/src/store/userMetadata/saveUserMetadataNatively/saveUserMetadataNatively.saga.ts @@ -8,9 +8,12 @@ import { createLogger } from '../../../utils/logger' const logger = createLogger('saveUserMetadataNativelySaga') export function* saveUserMetadataNativelySaga(action: PayloadAction): Generator { - logger.info('Storing user metadata in ios native storage', action.payload.profiles) + logger.info('Storing user metadata in ios native storage', action.payload.new, action.payload.updates) try { - const updates: string[] = action.payload.profiles.map(profile => JSON.stringify(profile)) + const updates: string[] = [ + ...action.payload.new.map(profile => JSON.stringify(profile)), + ...action.payload.updates.map(profile => JSON.stringify(profile)), + ] yield* call(NativeModules.CommunicationModule.saveUserMetadata, updates) } catch (e) { logger.error('Error while updating user metadata in ios native storage', e) diff --git a/packages/state-manager/package-lock.json b/packages/state-manager/package-lock.json index 190d9cfa2d..db39da7acc 100644 --- a/packages/state-manager/package-lock.json +++ b/packages/state-manager/package-lock.json @@ -14,6 +14,7 @@ "@reduxjs/toolkit": "^1.9.1", "factory-girl": "^5.0.4", "get-port": "^5.1.1", + "lodash": "^4.17.23", "luxon": "^2.0.2", "redux": "^4.1.1", "redux-persist": "^6.0.0", @@ -31,6 +32,7 @@ "@peculiar/webcrypto": "1.4.3", "@types/factory-girl": "^5.0.12", "@types/jest": "^26.0.24", + "@types/lodash": "^4.17.24", "@types/luxon": "^2.0.0", "@types/redux-saga": "^0.10.5", "babel-jest": "^29.3.1", @@ -4922,6 +4924,13 @@ "pretty-format": "^26.0.0" } }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/luxon": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.0.0.tgz", @@ -11378,10 +11387,10 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" }, "node_modules/lodash.isequal": { "version": "4.5.0", @@ -16579,6 +16588,12 @@ "pretty-format": "^26.0.0" } }, + "@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true + }, "@types/luxon": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.0.0.tgz", @@ -21516,10 +21531,9 @@ } }, "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" }, "lodash.isequal": { "version": "4.5.0", diff --git a/packages/state-manager/package.json b/packages/state-manager/package.json index a8279f49d1..6b0cb60265 100644 --- a/packages/state-manager/package.json +++ b/packages/state-manager/package.json @@ -33,6 +33,7 @@ "@reduxjs/toolkit": "^1.9.1", "factory-girl": "^5.0.4", "get-port": "^5.1.1", + "lodash": "^4.17.23", "luxon": "^2.0.2", "redux": "^4.1.1", "redux-persist": "^6.0.0", @@ -54,6 +55,7 @@ "@types/factory-girl": "^5.0.12", "@quiet/node-common": "^4.0.3", "@types/jest": "^26.0.24", + "@types/lodash": "^4.17.24", "@types/luxon": "^2.0.0", "@types/redux-saga": "^0.10.5", "babel-jest": "^29.3.1", diff --git a/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.ts b/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.ts index b46f698d33..b29a8e6abc 100644 --- a/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.ts +++ b/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.ts @@ -2,19 +2,24 @@ import { PayloadAction } from '@reduxjs/toolkit' import { createLogger } from '../../../utils/logger' import { apply, call, put, select } from 'typed-redux-saga' import { userProfileSelectors } from './userProfile.selectors' -import { SocketActions, SocketEvents, SocketEventsMap, UserProfile } from '@quiet/types' +import { SocketActions, SocketEvents, SocketEventsMap, UserProfile, UserProfilesUpdatedPayload } from '@quiet/types' import { applyEmitParams, Socket } from '../../../types' import { usersActions } from '../users.slice' +import * as _ from 'lodash' const logger = createLogger('updateUserProfilesSaga') export function* updateUserProfilesSaga(socket: Socket, action: PayloadAction): Generator { logger.info(`Updating user profiles (profile count = ${action.payload.length})`) - const userProfiles = yield* select(userProfileSelectors.userProfiles) - const updates = { ...userProfiles } + const existingProfiles = yield* select(userProfileSelectors.userProfiles) + const output: UserProfilesUpdatedPayload = { + new: [], + updates: [], + } + const updates = { ...existingProfiles } for (const userProfile of action.payload) { - if (updates[userProfile.userId]) { - const existingProfile = updates[userProfile.userId] + if (existingProfiles[userProfile.userId]) { + const existingProfile = existingProfiles[userProfile.userId] const updatedProfile = { ...existingProfile, @@ -42,20 +47,23 @@ export function* updateUserProfilesSaga(socket: Socket, action: PayloadAction 0 || output.updates.length > 0) { + logger.info(`Emitting user profiles updated event`, output.new, output.updates) + yield* apply(socket, socket.emit, applyEmitParams(SocketActions.USER_PROFILES_UPDATED, output)) + } else { + logger.trace('Skipping user profile updated event, no new or updated profiles') + } logger.info(`Done`) } diff --git a/packages/types/src/user.ts b/packages/types/src/user.ts index 72197c39ac..be8bd1da7b 100644 --- a/packages/types/src/user.ts +++ b/packages/types/src/user.ts @@ -60,7 +60,8 @@ export interface UserProfilesStoredEvent { } export interface UserProfilesUpdatedPayload { - profiles: UserProfile[] + new: UserProfile[] + updates: UserProfile[] } export interface UsersUpdatedEvent { From 35cb3c184248e26b7a58d81350b0df056b9b6e0f Mon Sep 17 00:00:00 2001 From: Isla <5048549+islathehut@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:27:06 -0400 Subject: [PATCH 13/17] Clean up logs and remove testing code --- packages/mobile/ios/CommunicationModule.swift | 10 ------- packages/mobile/ios/UserMetadataHandler.swift | 28 +++++++++++-------- .../saveUserMetadataNatively.saga.ts | 4 ++- .../userProfile/updateUserProfiles.saga.ts | 4 +-- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/packages/mobile/ios/CommunicationModule.swift b/packages/mobile/ios/CommunicationModule.swift index 1c25606062..b4eae33805 100644 --- a/packages/mobile/ios/CommunicationModule.swift +++ b/packages/mobile/ios/CommunicationModule.swift @@ -88,7 +88,6 @@ class CommunicationModule: RCTEventEmitter { let metadataAsString: String = metadataAsAny as! String let data = Data(metadataAsString.utf8) let decodedMetadata = try decoder.decode(UserMetadataStruct.self, from: data) - CommunicationModule.logger.info("Decoded user metadata: \(String(describing: decodedMetadata))") userMetadata.append(decodedMetadata) } catch { CommunicationModule.logger.error("Error while decoding user metadata: \(error)") @@ -100,15 +99,6 @@ class CommunicationModule: RCTEventEmitter { } catch { CommunicationModule.logger.error("Error while saving user metadata: \(error)") } - - for metadata in userMetadata { - do { - let stored = try self.userMetadataHandler.fetchUserMetadataById(userId: metadata.userId) - CommunicationModule.logger.info("Passed: \(String(describing: metadata)), Stored: \(String(describing: stored?.toStruct()))") - } catch { - // do nothing - } - } } @objc diff --git a/packages/mobile/ios/UserMetadataHandler.swift b/packages/mobile/ios/UserMetadataHandler.swift index fad190915c..0f8141858d 100644 --- a/packages/mobile/ios/UserMetadataHandler.swift +++ b/packages/mobile/ios/UserMetadataHandler.swift @@ -52,6 +52,10 @@ class ProfilePhoto: Identifiable { self.createdAt = createdAt } + public static func fromStruct(id: String, profilePhoto: ProfilePhotoStruct, createdAt: Date?) -> ProfilePhoto { + return ProfilePhoto(id: id, ext: profilePhoto.ext, path: profilePhoto.path, size: profilePhoto.size, width: profilePhoto.width, height: profilePhoto.height, createdAt: createdAt) + } + public func toStruct() -> ProfilePhotoStruct { return ProfilePhotoStruct(ext: self.ext, path: self.path, size: self.size, width: self.width, height: self.height) } @@ -68,9 +72,10 @@ class UserMetadata: Identifiable { init(id: String, nickname: String, profilePhoto: ProfilePhotoStruct?, createdAt: Date?) { var profilePhotoModel: ProfilePhoto? = nil - if (profilePhoto != nil) { - profilePhotoModel = ProfilePhoto(id: id, ext: profilePhoto!.ext, path: profilePhoto!.path, size: profilePhoto!.size, width: profilePhoto!.width, height: profilePhoto!.height, createdAt: createdAt) + if let unwrappedProfilePhoto = profilePhoto { + profilePhotoModel = ProfilePhoto.fromStruct(id: id, profilePhoto: unwrappedProfilePhoto, createdAt: createdAt) } + self.id = id self.nickname = nickname self.profilePhoto = profilePhotoModel @@ -124,13 +129,13 @@ class UserMetadataHandler: NSObject { UserMetadataHandler.logger.info("Inserting user metadata") for metadata in updatedMetadata { - UserMetadataHandler.logger.debug("Inserting data: \(String(describing: metadata))") - let model = UserMetadata.fromStruct(userMetadata: metadata, createdAt: Date.now) - let found = try self.fetchUserMetadataById(userId: model.id) - if (found != nil) { - UserMetadataHandler.logger.debug("Replacing existing metadata for \(model.id)") - try self.deleteUserMetadata(model: model) + UserMetadataHandler.logger.debug("Inserting data for \(metadata.userId)") + let found = try self.fetchUserMetadataById(userId: metadata.userId) + if let unwrappedFound = found { + UserMetadataHandler.logger.debug("Replacing existing metadata for \(metadata.userId)") + try self.deleteUserMetadata(model: unwrappedFound) } + let model = UserMetadata.fromStruct(userMetadata: metadata, createdAt: Date.now) context.insert(model) } @@ -159,8 +164,9 @@ class UserMetadataHandler: NSObject { return nil } guard models.count == 1 else { + UserMetadataHandler.logger.warning("Found \(models.count) stored metadata records for \(userId)") for model in models { - UserMetadataHandler.logger.warning("Found: \(String(describing: model.toStruct()))") + UserMetadataHandler.logger.warning("Found \(userId) created at \(model.createdAt?.ISO8601Format() ?? "NO CREATEDAT")") } throw UserMetadataError.incorrectModelCount(expected: 1, actual: models.count) } @@ -172,7 +178,7 @@ class UserMetadataHandler: NSObject { } public func deleteUserMetadata(model: UserMetadata) throws -> Void { - UserMetadataHandler.logger.info("Deleting UserMetadata record: \(String(describing: model.toStruct()))") + UserMetadataHandler.logger.info("Deleting UserMetadata record for \(model.id)") guard let context = self.modelContext else { throw UserMetadataError.missingModelContext @@ -182,7 +188,7 @@ class UserMetadataHandler: NSObject { context.delete(model) try context.save() } catch { - UserMetadataHandler.logger.error("Error while deleting UserMetadata \(String(describing: model.toStruct())): \(error)") + UserMetadataHandler.logger.error("Error while deleting UserMetadata for \(model.id): \(error)") throw UserMetadataError.unhandledError(reason: error) } } diff --git a/packages/mobile/src/store/userMetadata/saveUserMetadataNatively/saveUserMetadataNatively.saga.ts b/packages/mobile/src/store/userMetadata/saveUserMetadataNatively/saveUserMetadataNatively.saga.ts index dba157954a..f10b5b9123 100644 --- a/packages/mobile/src/store/userMetadata/saveUserMetadataNatively/saveUserMetadataNatively.saga.ts +++ b/packages/mobile/src/store/userMetadata/saveUserMetadataNatively/saveUserMetadataNatively.saga.ts @@ -8,7 +8,9 @@ import { createLogger } from '../../../utils/logger' const logger = createLogger('saveUserMetadataNativelySaga') export function* saveUserMetadataNativelySaga(action: PayloadAction): Generator { - logger.info('Storing user metadata in ios native storage', action.payload.new, action.payload.updates) + logger.info( + `Storing user metadata in ios native storage (new count = ${action.payload.new.length}, update count = ${action.payload.updates.length})` + ) try { const updates: string[] = [ ...action.payload.new.map(profile => JSON.stringify(profile)), diff --git a/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.ts b/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.ts index b29a8e6abc..1a143ecb0c 100644 --- a/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.ts +++ b/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.ts @@ -56,11 +56,11 @@ export function* updateUserProfilesSaga(socket: Socket, action: PayloadAction 0 || output.updates.length > 0) { - logger.info(`Emitting user profiles updated event`, output.new, output.updates) + logger.debug(`Emitting user profiles updated event`) yield* apply(socket, socket.emit, applyEmitParams(SocketActions.USER_PROFILES_UPDATED, output)) } else { logger.trace('Skipping user profile updated event, no new or updated profiles') From 71896802a85e3f067bedd16efbeaa0a1d338059b Mon Sep 17 00:00:00 2001 From: Isla <5048549+islathehut@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:05:26 -0400 Subject: [PATCH 14/17] Fix tests --- packages/backend/src/nest/qss/qss.service.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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)) From 7476ffce68ab8d1fa788a517f5c58ee944fd3e29 Mon Sep 17 00:00:00 2001 From: Isla <5048549+islathehut@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:07:28 -0400 Subject: [PATCH 15/17] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78d6b4c930..5b0bd4db0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * 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) +* Store user metadata in IOS native storage for notifications [#3091](https://github.com/TryQuiet/quiet/issues/3091) ### Fixes From edc9d9293c43f38f1d133049975b8b5b2c405faf Mon Sep 17 00:00:00 2001 From: Isla <5048549+islathehut@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:27:11 -0400 Subject: [PATCH 16/17] Update and move updateUserProfiles tests --- .../updateUserProfiles.saga.test.ts | 186 ++++++++++++++++++ .../src/sagas/users/users.slice.test.ts | 89 --------- .../src/utils/tests/factories.ts | 32 ++- packages/types/src/files.ts | 18 +- 4 files changed, 227 insertions(+), 98 deletions(-) create mode 100644 packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.test.ts delete mode 100644 packages/state-manager/src/sagas/users/users.slice.test.ts diff --git a/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.test.ts b/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.test.ts new file mode 100644 index 0000000000..48d176d5be --- /dev/null +++ b/packages/state-manager/src/sagas/users/userProfile/updateUserProfiles.saga.test.ts @@ -0,0 +1,186 @@ +import { expectSaga } from 'redux-saga-test-plan' +import { type FactoryGirl } from 'factory-girl' +import { UserProfile, SocketActions, Identity } from '@quiet/types' + +import { usersActions } from '../users.slice' +import { updateUserProfilesSaga } from './updateUserProfiles.saga' +import { MockedSocket } from '../../../utils/tests/mockedSocket' +import { Socket } from '../../../types' +import { Store } from '../../store.types' +import { prepareStore, testReducers } from '../../../utils/tests/prepareStore' +import { getBaseTypesFactory } from '../../../utils/tests/factories' +import { combineReducers } from 'redux' +import { userProfileSelectors } from './userProfile.selectors' +import { createLogger } from '../../../utils/logger' + +describe('updateUserProfilesSaga', () => { + let store: Store + let baseTypesFactory: FactoryGirl + let socket: MockedSocket + let userProfile: UserProfile + let userId: string + + const logger = createLogger('updateUserProfilesSaga:test') + + beforeEach(async () => { + socket = new MockedSocket() + store = prepareStore().store + baseTypesFactory = await getBaseTypesFactory() + + userProfile = await baseTypesFactory.create('UserProfile') + userId = userProfile.userId + }) + + it('should clear profilePhoto.path if CID changes', async () => { + const newCid = 'new-cid' + + const existingProfiles = { + [userId]: userProfile, + } + + const updatedProfile: UserProfile = { + ...userProfile, + profilePhoto: { + ...userProfile.profilePhoto!, + cid: newCid, + path: null, + }, + } + + let userProfilesSelectCalls = 0 + + await expectSaga( + updateUserProfilesSaga, + socket as unknown as Socket, + // @ts-ignore + usersActions.updateUserProfiles([updatedProfile]) + ) + .withReducer(combineReducers(testReducers)) + .withState(store.getState()) + .provide([ + { + select: ({ selector }: any, next: any) => { + if (selector === userProfileSelectors.userProfiles) { + userProfilesSelectCalls += 1 + return existingProfiles + } + return next() + }, + }, + ]) + .apply.like({ + context: socket, + fn: socket.emit, + args: [ + SocketActions.USER_PROFILES_UPDATED, + { + new: [], + updates: [updatedProfile], + }, + ], + }) + .put.like({ + action: { + type: usersActions.setUserProfiles.type, + payload: [updatedProfile], + }, + }) + .run() + }) + + it('should NOT clear profilePhoto.path if CID is the same', async () => { + const existingProfiles = { + [userId]: userProfile, + } + + const updatedProfile: UserProfile = { + ...userProfile, + profilePhoto: { + ...userProfile.profilePhoto!, + path: null, + }, + } + + let userProfilesSelectCalls = 0 + + await expectSaga( + updateUserProfilesSaga, + socket as unknown as Socket, + // @ts-ignore + usersActions.updateUserProfiles([updatedProfile]) + ) + .withReducer(combineReducers(testReducers)) + .withState(store.getState()) + .provide([ + { + select: ({ selector }: any, next: any) => { + if (selector === userProfileSelectors.userProfiles) { + userProfilesSelectCalls += 1 + return existingProfiles + } + return next() + }, + }, + ]) + // since we aren't updating the profile we aren't sending the socket event to ios + .not.apply.like({ + context: socket, + fn: socket.emit, + }) + .put.like({ + action: { + type: usersActions.setUserProfiles.type, + payload: [userProfile], + }, + }) + .run() + }) + + it('should send new profile via socket to ios', async () => { + const existingProfiles = { + [userId]: userProfile, + } + + const newProfile = await baseTypesFactory.create('UserProfile') + + let userProfilesSelectCalls = 0 + + await expectSaga( + updateUserProfilesSaga, + socket as unknown as Socket, + // @ts-ignore + { payload: [userProfile, newProfile] } + ) + .withReducer(combineReducers(testReducers)) + .withState(store.getState()) + .provide([ + { + select: ({ selector }: any, next: any) => { + if (selector === userProfileSelectors.userProfiles) { + userProfilesSelectCalls += 1 + return existingProfiles + } + return next() + }, + }, + ]) + .apply.like({ + context: socket, + fn: socket.emit, + args: [ + SocketActions.USER_PROFILES_UPDATED, + { + new: [newProfile], + updates: [], + }, + ], + }) + .put.like({ + action: { + type: usersActions.setUserProfiles.type, + payload: [userProfile, newProfile], + }, + }) + .run() + }) +}) diff --git a/packages/state-manager/src/sagas/users/users.slice.test.ts b/packages/state-manager/src/sagas/users/users.slice.test.ts deleted file mode 100644 index 915ece4adf..0000000000 --- a/packages/state-manager/src/sagas/users/users.slice.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { usersSlice, UsersState } from './users.slice' -import { UserProfile, FileMetadata } from '@quiet/types' - -describe('usersSlice', () => { - describe('updateUserProfiles', () => { - it('should clear profilePhoto.path if CID changes', () => { - const userId = 'user-1' - const oldCid = 'old-cid' - const newCid = 'new-cid' - - const initialState: UsersState = { - userProfiles: { - [userId]: { - userId, - nickname: 'alice', - profilePhoto: { - cid: oldCid, - path: '/path/to/old/photo.png', - name: 'photo', - ext: '.png', - message: { id: 'mid-1', channelId: 'PROFILE_PHOTO_CHANNEL_ID' }, - } as FileMetadata, - } as UserProfile, - }, - users: {}, - saveUserProfileError: null, - } - - const updatedProfile: UserProfile = { - userId, - nickname: 'alice', - profilePhoto: { - cid: newCid, - path: null, // Backend sends null path - name: 'photo', - ext: '.png', - message: { id: 'mid-2', channelId: 'PROFILE_PHOTO_CHANNEL_ID' }, - } as FileMetadata, - } - - const nextState = usersSlice.reducer(initialState, usersSlice.actions.updateUserProfiles([updatedProfile])) - - expect(nextState.userProfiles[userId].profilePhoto?.cid).toBe(newCid) - expect(nextState.userProfiles[userId].profilePhoto?.path).toBeNull() - }) - - it('should NOT clear profilePhoto.path if CID is the same', () => { - const userId = 'user-1' - const cid = 'same-cid' - const path = '/path/to/photo.png' - - const initialState: UsersState = { - userProfiles: { - [userId]: { - userId, - nickname: 'alice', - profilePhoto: { - cid, - path, - name: 'photo', - ext: '.png', - message: { id: 'mid-1', channelId: 'PROFILE_PHOTO_CHANNEL_ID' }, - } as FileMetadata, - } as UserProfile, - }, - users: {}, - saveUserProfileError: null, - } - - const updatedProfile: UserProfile = { - userId, - nickname: 'alice-updated', - profilePhoto: { - cid, - path: null, // Backend might send null path even if we have it locally - name: 'photo', - ext: '.png', - message: { id: 'mid-1', channelId: 'PROFILE_PHOTO_CHANNEL_ID' }, - } as FileMetadata, - } - - const nextState = usersSlice.reducer(initialState, usersSlice.actions.updateUserProfiles([updatedProfile])) - - expect(nextState.userProfiles[userId].nickname).toBe('alice-updated') - expect(nextState.userProfiles[userId].profilePhoto?.cid).toBe(cid) - expect(nextState.userProfiles[userId].profilePhoto?.path).toBe(path) - }) - }) -}) diff --git a/packages/state-manager/src/utils/tests/factories.ts b/packages/state-manager/src/utils/tests/factories.ts index 7659a6c395..57067a6a39 100644 --- a/packages/state-manager/src/utils/tests/factories.ts +++ b/packages/state-manager/src/utils/tests/factories.ts @@ -49,6 +49,8 @@ import { HCaptchaFormResponse, HCaptchaRequest, InviteResultWithSalt, + FileMessage, + FileEncryptionMetadata, } from '@quiet/types' import { InviteResult } from '@localfirst/auth' import { createLogger } from '../logger' @@ -66,6 +68,7 @@ import { errorsActions } from '../../sagas/errors/errors.slice' import { errorsSelectors } from '../../sagas/errors/errors.selectors' import { connectionActions } from '../../sagas/appConnection/connection.slice' import { connectionSelectors } from '../../sagas/appConnection/connection.selectors' +import { randomBytes } from 'crypto' const logger = createLogger('factories') @@ -125,11 +128,38 @@ export const getBaseTypesFactory = async () => { bio: factory.sequence('UserProfileDisplayData.bio', (n: number) => `bio_${n}`), }) + factory.define('FileMessage', Object, { + id: factory.sequence('FileMessage.id', (n: number) => `profile-photo-user-profile-photo-cid-${n}-${n}`), + channelId: '__profile-photo__', + }) + + factory.define('FileEncryptionMetadata', Object, { + header: factory.sequence('FileEncryptionMetadata.header', (n: number) => randomBytes(32).toString('base64')), + recipient: { + generation: 0, + type: 'ROLE', + name: 'MEMBER', + }, + }) + + factory.define('FileMetadata', Object, { + cid: factory.sequence('FileMetadata.cid', (n: number) => `user-profile-photo-cid-${n}`), + path: factory.sequence('FileMetadata.path', (n: number) => `/foo/bar/user-profile-photo-cid-${n}.png`), + ext: '.png', + name: factory.sequence('FileMetadata.name', (n: number) => `user-profile-photo-name-${n}`), + message: factory.assoc('FileMessage'), + size: factory.sequence('FileMetadata.size', (n: number) => 1024 + n), + width: factory.sequence('FileMetadata.width', (n: number) => 100 + n), + height: factory.sequence('FileMetadata.height', (n: number) => 100 + n), + enc: factory.assoc('FileEncryptionMetadata'), + }) + factory.define('UserProfile', Object, { userId: factory.sequence('UserProfile.userId', (n: number) => `userId_${n}`), nickname: factory.sequence('UserProfile.nickname', (n: number) => `userProfile.nickname_${n}`), - photo: 'dGVzdAo=', + photo: undefined, bio: factory.sequence('UserProfile.bio', (n: number) => `bio_${n}`), + profilePhoto: factory.assoc('FileMetadata'), }) factory.define('User', Object, { diff --git a/packages/types/src/files.ts b/packages/types/src/files.ts index 4997a9e7d5..cd7b823f27 100644 --- a/packages/types/src/files.ts +++ b/packages/types/src/files.ts @@ -11,20 +11,22 @@ export interface FilePreviewData { [id: string]: FileContent } +export interface FileEncryptionMetadata { + header: string + recipient: { + generation: number + type: string + name: string + } +} + export interface FileMetadata extends FileContent { cid: string message: FileMessage size?: number width?: number height?: number - enc?: { - header: string - recipient: { - generation: number - type: string - name: string - } - } + enc?: FileEncryptionMetadata } export interface AttachFilePayload { From 06bb47df0c555da5e81e63794092d6cf1527eea0 Mon Sep 17 00:00:00 2001 From: Isla <5048549+islathehut@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:29:06 -0400 Subject: [PATCH 17/17] Fix factories test --- .../state-manager/src/utils/tests/factories.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/state-manager/src/utils/tests/factories.ts b/packages/state-manager/src/utils/tests/factories.ts index 57067a6a39..74f5bd6f97 100644 --- a/packages/state-manager/src/utils/tests/factories.ts +++ b/packages/state-manager/src/utils/tests/factories.ts @@ -620,6 +620,21 @@ export const getSocketFactory = async () => { }, }) + factory.define(SocketActions.USER_PROFILES_UPDATED, Object, [ + { + profile: { + userId: 'user-id', + nickname: 'Test User', + photo: 'dGVzdAo=', + bio: 'This is a test user profile', + userData: { + onionAddress: 'test.onion', + peerId: 'peer-id', + }, + }, + }, + ]) + factory.define(`${SocketActions.SET_USER_PROFILE}_response`, Object, { success: true, error: undefined,