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,