merge master into badge-in-app-checkout

This commit is contained in:
spaced4ndy committed 2026-10-08 14:31:30 +04:00
commit 80b7f96a96
229 files changed
+8341 -1977

No files matched your search

+1
View File
@@ -123,6 +123,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
private func prepareForLaunch() {
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
excludeAppDataFromBackup()
}
static func keepScreenOn(_ on: Bool) {
+49 -12
View File
@@ -2391,6 +2391,10 @@ func startChat(refreshInvitations: Bool = true, onboarding: Bool = false) throws
m.chatRunning = true
chatLastStartGroupDefault.set(Date.now)
Task { await BadgeStore.shared.presentUnfinished() }
if shouldDeleteDatabaseBackupsDefault.get() {
deleteDatabaseBackups()
shouldDeleteDatabaseBackupsDefault.set(false)
}
}
func startChatWithTemporaryDatabase(ctrl: chat_ctrl) throws -> User? {
@@ -2952,23 +2956,19 @@ func processReceivedMsg(_ res: ChatEvent) async {
m.callInvitations[invitation.contact.id] = invitation
}
activateCall(invitation)
case let .callOffer(_, contact, callType, offer, sharedKey, _):
case let .callOffer(_, contact, callType, offer, sharedKey, askConfirmation):
await withCall(contact) { call in
await MainActor.run {
call.callState = .offerReceived
call.sharedKey = sharedKey
}
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
let iceServers = getIceServers()
logger.debug(".callOffer useRelay \(useRelay)")
logger.debug(".callOffer iceServers \(String(describing: iceServers))")
await m.callCommand.processCommand(.offer(
offer: offer.rtcSession,
iceCandidates: offer.rtcIceCandidates,
media: callType.media, aesKey: sharedKey,
iceServers: iceServers,
relay: useRelay
))
if askConfirmation {
showUnencryptedCallAlert(call) {
Task { await processCallOffer(callType, offer, sharedKey) }
}
} else {
await processCallOffer(callType, offer, sharedKey)
}
}
case let .callAnswer(_, contact, answer):
await withCall(contact) { call in
@@ -3108,6 +3108,43 @@ func processReceivedMsg(_ res: ChatEvent) async {
logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)")
}
}
func processCallOffer(_ callType: CallType, _ offer: WebRTCSession, _ sharedKey: String?) async {
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
let iceServers = getIceServers()
logger.debug(".callOffer useRelay \(useRelay)")
logger.debug(".callOffer iceServers \(String(describing: iceServers))")
await m.callCommand.processCommand(.offer(
offer: offer.rtcSession,
iceCandidates: offer.rtcIceCandidates,
media: callType.media, aesKey: sharedKey,
iceServers: iceServers,
relay: useRelay
))
}
func showUnencryptedCallAlert(_ call: Call, onContinue: @escaping () -> Void) {
DispatchQueue.main.async {
showAlert(
NSLocalizedString("Call is not encrypted", comment: "alert title"),
message: String.localizedStringWithFormat(NSLocalizedString("%@ accepted the call without end-to-end encryption.", comment: "alert message"), call.contact.displayName),
actions: {[
UIAlertAction(title: NSLocalizedString("End call", comment: "alert action"), style: .destructive) { _ in
// the call may have ended, or a new one started with the same contact, while the alert was shown
guard m.activeCall === call else { return }
if let uuid = call.callUUID {
CallController.shared.endCall(callUUID: uuid)
} else {
CallController.shared.endCall(call: call) {}
}
},
UIAlertAction(title: NSLocalizedString("Continue", comment: "alert action"), style: .default) { _ in
if m.activeCall === call { onContinue() }
}
]}
)
}
}
}
func switchToLocalSession() {
@@ -117,7 +117,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
encryptor.delegate = self
frameEncryptor = encryptor
let decryptor = RTCFrameDecryptor.init(sizeChange: -Int32(WebRTCClient.ivTagBytes))
let decryptor = RTCFrameDecryptor.init(sizeChange: 0)
decryptor.delegate = self
frameDecryptor = decryptor
}
@@ -508,11 +508,12 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func frameDecryptor(_ decryptor: RTCFrameDecryptor, mediaType: RTCRtpMediaType, withFrame encrypted: Data) -> Data? {
guard encrypted.count > 0 else { return nil }
let isKeyFrame = encrypted[0] & 1 == 0
let clearTextBytesSize = mediaType.rawValue == 0 ? 1 : isKeyFrame ? 10 : 3
guard encrypted.count >= clearTextBytesSize + WebRTCClient.ivTagBytes else { return nil }
if var key: [CChar] = activeCall?.aesKey?.cString(using: .utf8),
let pointer: UnsafeMutableRawPointer = malloc(encrypted.count) {
memcpy(pointer, (encrypted as NSData).bytes, encrypted.count)
let isKeyFrame = encrypted[0] & 1 == 0
let clearTextBytesSize = mediaType.rawValue == 0 ? 1 : isKeyFrame ? 10 : 3
logCrypto("decrypt", chat_decrypt_media(&key, pointer.advanced(by: clearTextBytesSize), Int32(encrypted.count - clearTextBytesSize)))
return Data(bytes: pointer, count: encrypted.count - WebRTCClient.ivTagBytes)
} else {
@@ -522,11 +523,12 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func frameEncryptor(_ encryptor: RTCFrameEncryptor, mediaType: RTCRtpMediaType, withFrame unencrypted: Data) -> Data? {
guard unencrypted.count > 0 else { return nil }
let isKeyFrame = unencrypted[0] & 1 == 0
let clearTextBytesSize = mediaType.rawValue == 0 ? 1 : isKeyFrame ? 10 : 3
guard unencrypted.count >= clearTextBytesSize else { return nil }
if var key: [CChar] = activeCall?.aesKey?.cString(using: .utf8),
let pointer: UnsafeMutableRawPointer = malloc(unencrypted.count + WebRTCClient.ivTagBytes) {
memcpy(pointer, (unencrypted as NSData).bytes, unencrypted.count)
let isKeyFrame = unencrypted[0] & 1 == 0
let clearTextBytesSize = mediaType.rawValue == 0 ? 1 : isKeyFrame ? 10 : 3
logCrypto("encrypt", chat_encrypt_media(chat_ctrl, &key, pointer.advanced(by: clearTextBytesSize), Int32(unencrypted.count + WebRTCClient.ivTagBytes - clearTextBytesSize)))
return Data(bytes: pointer, count: unencrypted.count + WebRTCClient.ivTagBytes)
} else {
@@ -22,11 +22,11 @@ struct GetStakeBanner: View {
Button(action: onTap) {
HStack(spacing: 0) {
VStack(alignment: .leading, spacing: 4) {
Text("Get a stake in SimpleX Chat!")
Text("Invest in SimpleX Chat!")
.font(.headline)
.foregroundColor(theme.colors.primary)
.lineLimit(2)
Text("Invest on Wefunder from $100")
Text("Crowdfunding on Wefunder")
.font(.subheadline)
.foregroundColor(theme.colors.onBackground)
.lineLimit(2)
@@ -153,6 +153,9 @@ struct DatabaseEncryptionView: View {
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
}
try await apiStorageEncryption(currentKey: currentKey, newKey: newKey)
if currentKey != newKey {
shouldDeleteDatabaseBackupsDefault.set(true)
}
encryptionStartedDefault.set(false)
initialRandomDBPassphraseGroupDefault.set(false)
if migration {
@@ -118,6 +118,9 @@ struct DatabaseErrorView: View {
case .errorKeychain:
titleText("Keychain error")
errorView(Text("Cannot access keychain to save database password"))
case .errorKeyGeneration:
titleText("Database error")
errorView(Text("Cannot generate random database passphrase"))
case .invalidConfirmation:
// this can only happen if incorrect parameter is passed
titleText("Invalid migration confirmation")
@@ -217,6 +220,8 @@ struct DatabaseErrorView: View {
)
case .errorKeychain:
am.showAlertMsg(title: "Keychain error")
case .errorKeyGeneration:
am.showAlertMsg(title: "Database error", message: "Cannot generate random database passphrase")
case let .errorSQL(_, error):
am.showAlert(Alert(
title: Text("Database error"),
@@ -453,6 +453,10 @@ struct DatabaseView: View {
ChatReceiver.shared.start()
chatLastStartGroupDefault.set(Date.now)
AppChatState.shared.set(.active)
if shouldDeleteDatabaseBackupsDefault.get() {
deleteDatabaseBackups()
shouldDeleteDatabaseBackupsDefault.set(false)
}
} catch let error {
runChat.wrappedValue = false
showAlert(NSLocalizedString("Error starting chat", comment: ""), message: responseError(error))
@@ -709,6 +709,8 @@ private func showErrorOnMigrationIfNeeded(_ status: DBMigrationResult, _ alert:
alert.wrappedValue = .wrongPassphrase()
case .errorKeychain:
alert.wrappedValue = .keychainError()
case .errorKeyGeneration:
alert.wrappedValue = .databaseError(message: NSLocalizedString("Cannot generate random database passphrase", comment: "alert message"))
case let .errorSQL(_, error):
alert.wrappedValue = .databaseError(message: error)
case let .unknown(error):
@@ -539,7 +539,7 @@ struct MigrateToDevice: View {
Task {
do {
if !hasChatCtrl() {
chatInitControllerRemovingDatabases()
try chatInitControllerRemovingDatabases()
} else if ChatModel.shared.chatRunning == true {
// cannot delete storage if chat is running
try await stopChatAsync()
@@ -735,6 +735,8 @@ private func showErrorOnMigrationIfNeeded(_ status: DBMigrationResult, _ alert:
alert.wrappedValue = .wrongPassphrase()
case .errorKeychain:
alert.wrappedValue = .keychainError()
case .errorKeyGeneration:
alert.wrappedValue = .databaseError(message: NSLocalizedString("Cannot generate random database passphrase", comment: "alert message"))
case let .errorSQL(_, error):
alert.wrappedValue = .databaseError(message: error)
case let .unknown(error):
@@ -890,7 +890,7 @@ struct GetStakeView: View {
var body: some View {
ZoomablePageView {
VStack(alignment: .leading, spacing: 18) {
Text(verbatim: "Get a stake in\nSimpleX Chat")
Text(verbatim: "Invest in\nSimpleX Chat")
.font(.largeTitle)
.bold()
.fixedSize(horizontal: false, vertical: true)
@@ -13,6 +13,8 @@ import SimpleXChat
let simplexTeamURL = URL(string: "simplex:/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw?h=smp6.simplex.im")!
let simplexNewsURL = URL(string: "simplex:/c#grcfG3ulVI4Sh6ow33qBsmSk7uEy3gRSl2KkJ5ER6tA?h=smp18.simplex.im")!
let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
let appBuild = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
@@ -47,6 +49,7 @@ let DEFAULT_SHOULD_IMPORT_APP_SETTINGS = "shouldImportAppSettings"
let DEFAULT_DEVELOPER_TOOLS = "developerTools"
let DEFAULT_ENCRYPTION_STARTED = "encryptionStarted"
let DEFAULT_ENCRYPTION_STARTED_AT = "encryptionStartedAt"
let DEFAULT_SHOULD_DELETE_DATABASE_BACKUPS = "shouldDeleteDatabaseBackups"
let DEFAULT_ACCENT_COLOR_RED = "accentColorRed" // deprecated, only used for migration
let DEFAULT_ACCENT_COLOR_GREEN = "accentColorGreen" // deprecated, only used for migration
let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue" // deprecated, only used for migration
@@ -205,6 +208,8 @@ let encryptionStartedDefault = BoolDefault(defaults: UserDefaults.standard, forK
let encryptionStartedAtDefault = DateDefault(defaults: UserDefaults.standard, forKey: DEFAULT_ENCRYPTION_STARTED_AT)
let shouldDeleteDatabaseBackupsDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOULD_DELETE_DATABASE_BACKUPS)
let connectViaLinkTabDefault = EnumDefault<ConnectViaLinkTab>(defaults: UserDefaults.standard, forKey: DEFAULT_CONNECT_VIA_LINK_TAB, withDefault: .scan)
let privacySimplexLinkModeDefault = EnumDefault<SimpleXLinkMode>(defaults: UserDefaults.standard, forKey: DEFAULT_PRIVACY_SIMPLEX_LINK_MODE, withDefault: .description)
@@ -462,6 +467,15 @@ struct SettingsView: View {
}
Section(header: Text("Contact").foregroundColor(theme.colors.secondary)) {
settingsRow("antenna.radiowaves.left.and.right", color: theme.colors.secondary) {
Button("Follow SimpleX Network News") {
dismiss()
DispatchQueue.main.async {
ChatModel.shared.appOpenUrl = simplexNewsURL
}
}
}
.disabled(chatModel.chatRunning != true)
settingsRow("number", color: theme.colors.secondary) {
Button("Send questions and ideas") {
dismiss()
+5
View File
@@ -247,6 +247,11 @@ class ShareModel: ObservableObject {
title: "Keychain error",
message: "Cannot access keychain to save database password"
)
case .errorKeyGeneration:
ErrorAlert(
title: "Database error",
message: "Cannot generate random database passphrase"
)
case .invalidConfirmation:
ErrorAlert("Invalid migration confirmation")
case let .unknown(json):
+18 -18
View File
@@ -195,8 +195,8 @@
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; };
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; };
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f.a */; };
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.10-1nt2m8GM0NE8NSirknFyt8-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.10-1nt2m8GM0NE8NSirknFyt8-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.10-1nt2m8GM0NE8NSirknFyt8.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.10-1nt2m8GM0NE8NSirknFyt8.a */; };
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; };
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
@@ -590,8 +590,8 @@
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f.a"; sourceTree = "<group>"; };
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.10-1nt2m8GM0NE8NSirknFyt8-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.1.0.10-1nt2m8GM0NE8NSirknFyt8-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.10-1nt2m8GM0NE8NSirknFyt8.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.1.0.10-1nt2m8GM0NE8NSirknFyt8.a"; sourceTree = "<group>"; };
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
@@ -764,8 +764,8 @@
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.10-1nt2m8GM0NE8NSirknFyt8-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.10-1nt2m8GM0NE8NSirknFyt8.a in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -852,8 +852,8 @@
64C829992D54AEEE006B9E89 /* libffi.a */,
64C829982D54AEED006B9E89 /* libgmp.a */,
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.7-KLlSPSHmTF28mDmnoCMp3f.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.10-1nt2m8GM0NE8NSirknFyt8-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.1.0.10-1nt2m8GM0NE8NSirknFyt8.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -2147,7 +2147,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 357;
CURRENT_PROJECT_VERSION = 362;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -2197,7 +2197,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 357;
CURRENT_PROJECT_VERSION = 362;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -2239,7 +2239,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 357;
CURRENT_PROJECT_VERSION = 362;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@@ -2259,7 +2259,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 357;
CURRENT_PROJECT_VERSION = 362;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@@ -2284,7 +2284,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 357;
CURRENT_PROJECT_VERSION = 362;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -2321,7 +2321,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 357;
CURRENT_PROJECT_VERSION = 362;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -2358,7 +2358,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 357;
CURRENT_PROJECT_VERSION = 362;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2409,7 +2409,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 357;
CURRENT_PROJECT_VERSION = 362;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2463,7 +2463,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 357;
CURRENT_PROJECT_VERSION = 362;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2497,7 +2497,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 357;
CURRENT_PROJECT_VERSION = 362;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
+10 -4
View File
@@ -32,7 +32,12 @@ public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: Migratio
} else if useKeychain {
if !hasDatabase() {
logger.debug("chatMigrateInit generating a random DB key")
dbKey = randomDatabasePassword()
guard let key = randomDatabasePassword() else {
let result = (false, DBMigrationResult.errorKeyGeneration)
migrationResult = result
return result
}
dbKey = key
initialRandomDBPassphraseGroupDefault.set(true)
} else if let key = kcDatabasePassword.get() {
dbKey = key
@@ -56,7 +61,7 @@ public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: Migratio
public func chatInitTemporaryDatabase(url: URL, key: String? = nil, confirmation: MigrationConfirmation = .error) -> (DBMigrationResult, chat_ctrl?) {
let dbPath = url.path
let dbKey = key ?? randomDatabasePassword()
guard let dbKey = key ?? randomDatabasePassword() else { return (.errorKeyGeneration, nil) }
logger.debug("chatInitTemporaryDatabase path: \(dbPath)")
var temporaryController: chat_ctrl? = nil
var cPath = dbPath.cString(using: .utf8)!
@@ -66,14 +71,14 @@ public func chatInitTemporaryDatabase(url: URL, key: String? = nil, confirmation
return (dbMigrationResult(dataFromCString(cjson)), temporaryController)
}
public func chatInitControllerRemovingDatabases() {
public func chatInitControllerRemovingDatabases() throws {
let dbPath = getAppDatabasePath().path
let fm = FileManager.default
// Remove previous databases, otherwise, can be .errorNotADatabase with nil controller
try? fm.removeItem(atPath: dbPath + CHAT_DB)
try? fm.removeItem(atPath: dbPath + AGENT_DB)
let dbKey = randomDatabasePassword()
guard let dbKey = randomDatabasePassword() else { throw RuntimeError("Cannot generate random database passphrase") }
logger.debug("chatInitControllerRemovingDatabases path: \(dbPath)")
var cPath = dbPath.cString(using: .utf8)!
var cKey = dbKey.cString(using: .utf8)!
@@ -353,6 +358,7 @@ public enum DBMigrationResult: Decodable, Equatable {
case errorMigration(dbFile: String, migrationError: MigrationError)
case errorSQL(dbFile: String, migrationSQLError: String)
case errorKeychain
case errorKeyGeneration
case unknown(json: String)
}
+3
View File
@@ -3904,6 +3904,7 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
case .memberCreatedContact: return false
case .memberProfileUpdated: return false
case .newMemberPendingReview: return true
case .msgBadSignature: return false
}
case .sndGroupEvent: return false
case .rcvConnEvent: return false
@@ -6075,6 +6076,7 @@ public enum RcvGroupEvent: Decodable, Hashable {
case memberCreatedContact
case memberProfileUpdated(fromProfile: Profile, toProfile: Profile)
case newMemberPendingReview
case msgBadSignature
var text: String { text(isChannel: false) }
@@ -6110,6 +6112,7 @@ public enum RcvGroupEvent: Decodable, Hashable {
case .memberCreatedContact: return NSLocalizedString("requested connection", comment: "rcv group event chat item")
case let .memberProfileUpdated(fromProfile, toProfile): return profileUpdatedText(fromProfile, toProfile)
case .newMemberPendingReview: return NSLocalizedString("New member wants to join the group.", comment: "rcv group event chat item")
case .msgBadSignature: return NSLocalizedString("message rejected: bad signature", comment: "rcv group event chat item")
}
}
+27
View File
@@ -50,6 +50,10 @@ private let CHAT_DB_BAK: String = "_chat.db.bak"
private let AGENT_DB_BAK: String = "_agent.db.bak"
private let CHAT_DB_EXPORTED: String = "_chat.db.exported"
private let AGENT_DB_EXPORTED: String = "_agent.db.exported"
// Spec: spec/database.md#getDocumentsDirectory
public func getDocumentsDirectory() -> URL {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
@@ -66,6 +70,18 @@ func getAppDirectory() -> URL {
: getDocumentsDirectory()
}
public func excludeAppDataFromBackup() {
var values = URLResourceValues()
values.isExcludedFromBackup = true
for var dir in [getGroupContainerDirectory(), getDocumentsDirectory()] {
do {
try dir.setResourceValues(values)
} catch {
logger.error("FileUtils.excludeAppDataFromBackup error: \(error.localizedDescription)")
}
}
}
// Spec: spec/database.md#DB_FILE_PREFIX
let DB_FILE_PREFIX = "simplex_v1"
@@ -101,6 +117,8 @@ public func deleteAppDatabaseAndFiles() {
}
try? fm.removeItem(atPath: dbPath + CHAT_DB_BAK)
try? fm.removeItem(atPath: dbPath + AGENT_DB_BAK)
try? fm.removeItem(atPath: dbPath + CHAT_DB_EXPORTED)
try? fm.removeItem(atPath: dbPath + AGENT_DB_EXPORTED)
try? fm.removeItem(at: getTempFilesDirectory())
try? fm.removeItem(at: getMigrationTempFilesDirectory())
try? fm.createDirectory(at: getTempFilesDirectory(), withIntermediateDirectories: true)
@@ -176,6 +194,13 @@ private func restoreBackupFile(fromPath: String, toPath: String) throws {
try fm.copyItem(atPath: fromPath, toPath: toPath)
}
public func deleteDatabaseBackups() {
let fm = FileManager.default
let dbPath = getAppDatabasePath().path
try? fm.removeItem(atPath: dbPath + CHAT_DB_BAK)
try? fm.removeItem(atPath: dbPath + AGENT_DB_BAK)
}
public func hasLegacyDatabase() -> Bool {
hasDatabaseAtPath(getLegacyDatabasePath())
}
@@ -198,6 +223,8 @@ public func removeLegacyDatabaseAndFiles() -> Bool {
let r2 = nil != (try? fm.removeItem(atPath: dbPath.path + CHAT_DB))
try? fm.removeItem(atPath: dbPath.path + AGENT_DB_BAK)
try? fm.removeItem(atPath: dbPath.path + CHAT_DB_BAK)
try? fm.removeItem(atPath: dbPath.path + AGENT_DB_EXPORTED)
try? fm.removeItem(atPath: dbPath.path + CHAT_DB_EXPORTED)
try? fm.removeItem(at: appFiles)
return r1 && r2
}
+2 -2
View File
@@ -37,7 +37,7 @@ public struct KeyChainItem {
}
}
func randomDatabasePassword() -> String {
func randomDatabasePassword() -> String? {
var keyData = Data(count: 32)
let status = keyData.withUnsafeMutableBytes {
SecRandomCopyBytes(kSecRandomDefault, 32, $0.baseAddress!)
@@ -46,7 +46,7 @@ func randomDatabasePassword() -> String {
return keyData.base64EncodedString()
} else {
logger.error("randomDatabasePassword: error \(status)")
return ""
return nil
}
}
+1 -1
View File
@@ -138,7 +138,7 @@ public func createErrorNtf(_ dbStatus: DBMigrationResult, _ badgeCount: Int) ->
title = NSLocalizedString("Encrypted message: no passphrase", comment: "notification")
case .errorMigration:
title = NSLocalizedString("Encrypted message: database migration error", comment: "notification")
case .errorSQL:
case .errorSQL, .errorKeyGeneration:
title = NSLocalizedString("Encrypted message: database error", comment: "notification")
case .errorKeychain:
title = NSLocalizedString("Encrypted message: keychain error", comment: "notification")
+5
View File
@@ -74,6 +74,10 @@ See [`getDocumentsDirectory()`](../SimpleXChat/FileUtils.swift#L47) and [`getGro
The container choice is stored in `dbContainerGroupDefault` (`GroupDefaults`).
### Backup Exclusion
`isExcludedFromBackup` is set on both containers by [`excludeAppDataFromBackup()`](../SimpleXChat/FileUtils.swift#L69-L79), which is called from [`prepareForLaunch()`](../Shared/AppDelegate.swift#L124-L127) on each app launch. Files that the app, NSE and SE create or replace in these directories later are excluded as well.
---
## 3. Haskell Store Modules
@@ -130,6 +134,7 @@ Migration results are decoded in Swift as `DBMigrationResult`:
- `.errorMigration(dbFile:, migrationError:)` -- migration failed
- `.errorSQL(dbFile:, migrationSQLError:)` -- SQL error during migration
- `.errorKeychain` -- keychain access failed
- `.errorKeyGeneration` -- random database key generation failed
- `.unknown(json:)` -- unrecognized response
---