From 960482f527b7d5cc01a0df6a8594b8b3826d978b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 26 Aug 2023 12:03:45 +0100 Subject: [PATCH 1/8] docs: correct year in privacy policy --- PRIVACY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRIVACY.md b/PRIVACY.md index ff16e09afe..dbd48940f6 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -131,4 +131,4 @@ You accept our Terms of Service ("Terms") by installing or using any of our apps **Ending these Terms**. You may end these Terms with SimpleX Chat at any time by deleting SimpleX Chat app(s) from your device and discontinuing use of our Services. The provisions related to Licenses, Disclaimers, Limitation of Liability, Resolving dispute, Availability, Changes to the terms, Enforcing the terms, and Ending these Terms will survive termination of your relationship with SimpleX Chat. -Updated August 17, 2022 +Updated August 17, 2023 From 7504a82cb3f29af265eb80a3696c1b3e17fe1546 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 26 Aug 2023 21:00:26 +0100 Subject: [PATCH 2/8] v5.3.0-beta.6: ios 169, android 148, desktop 1.4.0 (6) --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 65 +++++++++------------- apps/multiplatform/gradle.properties | 8 +-- 2 files changed, 30 insertions(+), 43 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index bb2f032443..8b814e3c0d 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -77,6 +77,11 @@ 5C9CC7A928C532AB00BEF955 /* DatabaseErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */; }; 5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; }; 5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; }; + 5C9F83F42A9A7D98009AD0AA /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F83EF2A9A7D98009AD0AA /* libffi.a */; }; + 5C9F83F52A9A7D98009AD0AA /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F83F02A9A7D98009AD0AA /* libgmp.a */; }; + 5C9F83F62A9A7D98009AD0AA /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F83F12A9A7D98009AD0AA /* libgmpxx.a */; }; + 5C9F83F72A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F83F22A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h-ghc8.10.7.a */; }; + 5C9F83F82A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F83F32A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h.a */; }; 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; }; 5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; }; 5CA059DE279559F40002BEB4 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */; }; @@ -87,7 +92,6 @@ 5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79929211BB900072E13 /* PreferencesView.swift */; }; 5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */; }; 5CB0BA882826CB3A00B3292C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CB0BA862826CB3A00B3292C /* InfoPlist.strings */; }; - 5CB0BA8B2826CB3A00B3292C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CB0BA892826CB3A00B3292C /* Localizable.strings */; }; 5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA8D2827126500B3292C /* OnboardingView.swift */; }; 5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */; }; 5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA91282713FD00B3292C /* CreateProfile.swift */; }; @@ -161,11 +165,6 @@ 644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */; }; 644EFFE42937BE9700525D5B /* MarkedDeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */; }; 6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; }; - 6462EF7A2A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6462EF752A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a */; }; - 6462EF7B2A8F4448003B2EAF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6462EF762A8F4448003B2EAF /* libgmp.a */; }; - 6462EF7C2A8F4448003B2EAF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6462EF772A8F4448003B2EAF /* libgmpxx.a */; }; - 6462EF7D2A8F4448003B2EAF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6462EF782A8F4448003B2EAF /* libffi.a */; }; - 6462EF7E2A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6462EF792A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a */; }; 646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */; }; 646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; }; 647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */; }; @@ -297,11 +296,9 @@ 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileImage.swift; sourceTree = ""; }; 5C65DAE429C77136003CEE45 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; - 5C65DAE529C77136003CEE45 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 5C65DAE629C771B9003CEE45 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5C65DAE729C771B9003CEE45 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; 5C65DAEA29CB8867003CEE45 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; - 5C65DAEB29CB8867003CEE45 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; 5C65DAEC29CB8908003CEE45 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = "es.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5C65DAED29CB8908003CEE45 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; 5C65DAF829D0CC20003CEE45 /* DeveloperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperView.swift; sourceTree = ""; }; @@ -316,11 +313,9 @@ 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = ""; }; 5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = ""; }; 5C84FE9129A216C800D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; - 5C84FE9229A216C800D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; 5C84FE9329A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = "nl.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5C84FE9429A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = ""; }; 5C8B41C929AF41BC00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; - 5C8B41CA29AF41BC00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 5C8B41CB29AF44CF00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = "cs.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5C8B41CC29AF44CF00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = ""; }; 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolServersView.swift; sourceTree = ""; }; @@ -335,8 +330,12 @@ 5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkAndServers.swift; sourceTree = ""; }; 5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseErrorView.swift; sourceTree = ""; }; 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = ""; }; - 5C9CC7B128D1F8F400BEF955 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = ""; }; + 5C9F83EF2A9A7D98009AD0AA /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5C9F83F02A9A7D98009AD0AA /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5C9F83F12A9A7D98009AD0AA /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5C9F83F22A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h-ghc8.10.7.a"; sourceTree = ""; }; + 5C9F83F32A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h.a"; sourceTree = ""; }; 5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = ""; }; 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXApp.swift; sourceTree = ""; }; @@ -347,24 +346,19 @@ 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOS.swift; sourceTree = ""; }; 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOSLaunchTests.swift; sourceTree = ""; }; 5CA3ED4D2A942170005D71E2 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = ""; }; - 5CA3ED4E2A942170005D71E2 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = ""; }; 5CA3ED4F2A9422D1005D71E2 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = "th.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5CA3ED502A9422D1005D71E2 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = ""; }; 5CA7DFC229302AF000F7FDDE /* AppSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSheet.swift; sourceTree = ""; }; 5CA85D0A297218AA0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; - 5CA85D0B297218AA0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; 5CA85D0C297219EF0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = "it.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5CA85D0D297219EF0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; 5CAB912529E93F9400F34A95 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; - 5CAB912629E93F9400F34A95 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; 5CAC41182A192D8400C331A2 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; - 5CAC41192A192D8400C331A2 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; 5CAC411A2A192DE800C331A2 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = "ja.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5CAC411B2A192DE800C331A2 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = ""; }; 5CADE79929211BB900072E13 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = ""; }; 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactPreferencesView.swift; sourceTree = ""; }; 5CB0BA872826CB3A00B3292C /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; - 5CB0BA8A2826CB3A00B3292C /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 5CB0BA8D2827126500B3292C /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; 5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXInfo.swift; sourceTree = ""; }; 5CB0BA91282713FD00B3292C /* CreateProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfile.swift; sourceTree = ""; }; @@ -373,7 +367,6 @@ 5CB2085028DB64CA00D024EC /* CreateLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateLinkView.swift; sourceTree = ""; }; 5CB2085228DB7CAF00D024EC /* ConnectViaLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViaLinkView.swift; sourceTree = ""; }; 5CB2085428DE647400D024EC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; - 5CB2085528DE647400D024EC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = ""; }; 5CB346E62868D76D001FD2EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushEnvironment.swift; sourceTree = ""; }; @@ -385,7 +378,6 @@ 5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; 5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListNavLink.swift; sourceTree = ""; }; 5CBD285529565CAE00EC2CF4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; - 5CBD285629565CAE00EC2CF4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; 5CBD285729565D2600EC2CF4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = "fr.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5CBD285829565D2600EC2CF4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = ""; }; 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUtils.swift; sourceTree = ""; }; @@ -442,11 +434,6 @@ 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramedCIVoiceView.swift; sourceTree = ""; }; 644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkedDeletedItemView.swift; sourceTree = ""; }; 6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = ""; }; - 6462EF752A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a"; sourceTree = ""; }; - 6462EF762A8F4448003B2EAF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 6462EF772A8F4448003B2EAF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 6462EF782A8F4448003B2EAF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 6462EF792A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a"; sourceTree = ""; }; 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/System/Library/Frameworks/LocalAuthentication.framework; sourceTree = DEVELOPER_DIR; }; 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = ""; }; 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberInfoView.swift; sourceTree = ""; }; @@ -505,12 +492,12 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5C9F83F82A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 6462EF7D2A8F4448003B2EAF /* libffi.a in Frameworks */, - 6462EF7C2A8F4448003B2EAF /* libgmpxx.a in Frameworks */, - 6462EF7A2A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a in Frameworks */, - 6462EF7E2A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a in Frameworks */, - 6462EF7B2A8F4448003B2EAF /* libgmp.a in Frameworks */, + 5C9F83F72A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h-ghc8.10.7.a in Frameworks */, + 5C9F83F62A9A7D98009AD0AA /* libgmpxx.a in Frameworks */, + 5C9F83F42A9A7D98009AD0AA /* libffi.a in Frameworks */, + 5C9F83F52A9A7D98009AD0AA /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -572,11 +559,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 6462EF782A8F4448003B2EAF /* libffi.a */, - 6462EF762A8F4448003B2EAF /* libgmp.a */, - 6462EF772A8F4448003B2EAF /* libgmpxx.a */, - 6462EF752A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA-ghc8.10.7.a */, - 6462EF792A8F4448003B2EAF /* libHSsimplex-chat-5.3.0.5-AGHrmoVFP0r7R9kWFmg3UA.a */, + 5C9F83EF2A9A7D98009AD0AA /* libffi.a */, + 5C9F83F02A9A7D98009AD0AA /* libgmp.a */, + 5C9F83F12A9A7D98009AD0AA /* libgmpxx.a */, + 5C9F83F22A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h-ghc8.10.7.a */, + 5C9F83F32A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h.a */, ); path = Libraries; sourceTree = ""; @@ -1466,7 +1453,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 168; + CURRENT_PROJECT_VERSION = 169; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1508,7 +1495,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 168; + CURRENT_PROJECT_VERSION = 169; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1588,7 +1575,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 168; + CURRENT_PROJECT_VERSION = 169; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1620,7 +1607,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 168; + CURRENT_PROJECT_VERSION = 169; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1652,7 +1639,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 167; + CURRENT_PROJECT_VERSION = 169; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1698,7 +1685,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 167; + CURRENT_PROJECT_VERSION = 169; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index c7d7ed736e..e2944a62f0 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -25,11 +25,11 @@ android.nonTransitiveRClass=true android.enableJetifier=true kotlin.mpp.androidSourceSetLayoutVersion=2 -android.version_name=5.3-beta.5 -android.version_code=147 +android.version_name=5.3-beta.6 +android.version_code=148 -desktop.version_name=1.3.0 -desktop.version_code=5 +desktop.version_name=1.4.0 +desktop.version_code=6 kotlin.version=1.8.20 gradle.plugin.version=7.4.2 From 10f8b8086ec4fcea6a42ac62335ed5d74d24cc10 Mon Sep 17 00:00:00 2001 From: "M. Sarmad Qadeer" Date: Sun, 27 Aug 2023 21:45:13 +0500 Subject: [PATCH 3/8] Sq/website fdroid page (#2980) * website: change height of f-droid page main section * website: update lang strings for f-droid page * website: fix fdroid page broken in other languages * update translations * website: add language navigation to fdroid page link --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- website/.eleventy.js | 2 +- website/langs/ar.json | 10 ++-------- website/langs/de.json | 10 +++------- website/langs/en.json | 8 ++------ website/langs/es.json | 10 +++------- website/langs/fr.json | 10 +++------- website/langs/it.json | 10 +++------- website/langs/nl.json | 10 +++------- website/langs/uk.json | 10 +++------- website/src/_includes/contact_page.html | 5 +++-- website/src/_includes/hero.html | 3 ++- .../src/_includes/sections/join_simplex.html | 4 +++- website/src/fdroid.html | 17 ++++++++++++++--- website/web.sh | 1 + 14 files changed, 46 insertions(+), 64 deletions(-) diff --git a/website/.eleventy.js b/website/.eleventy.js index fbfc0d71c8..fb9fe108f2 100644 --- a/website/.eleventy.js +++ b/website/.eleventy.js @@ -52,7 +52,7 @@ const globalConfig = { } const translationsDirectoryPath = './langs' -const supportedRoutes = ["blog", "contact", "invitation", "docs", ""] +const supportedRoutes = ["blog", "contact", "invitation", "docs", "fdroid", ""] let supportedLangs = [] fs.readdir(translationsDirectoryPath, (err, files) => { if (err) { diff --git a/website/langs/ar.json b/website/langs/ar.json index fd6cf4eb84..afa1076a6f 100644 --- a/website/langs/ar.json +++ b/website/langs/ar.json @@ -237,14 +237,8 @@ "simplex-chat-via-f-droid": "SimpleX Chat عبر F-Droid", "simplex-chat-repo": "مستودع SimpleX Chat", "stable-and-beta-versions-built-by-developers": "الإصدارات الثابتة والتجريبية التي أنشأها المطورون", - "to-add-it-to-your-f-droid-client": "لإضافته إلى عميل F-Droid الخاص بك", - "scan-the-qr-code-or": "مسح رمز QR أو", - "use-this-url": "استخدم عنوان URL هذا", "signing-key-fingerprint": "توقيع مفتاح البصمة (SHA-256)", "f-droid-org-repo": "مستودع F-Droid.org", "stable-versions-built-by-f-droid-org": "الإصدارات الثابتة التي تم إنشاؤها بواسطة F-Droid.org", - "releases-to-this-repo-are-done-1-2-days-later": "يتم إصدار الإصدارات إلى هذا المستودع بعد يوم أو يومين", - "simplex-and-f-droid-repo-sign-builds-with-different-keys": "يتم إنشاء علامة المستودعات SimpleX Chat و F-Droid.org بمفاتيح مختلفة. للتبديل، من فضلك", - "export": "تصدير", - "chat-db-and-re-install-app": "قاعدة بيانات الدردشة وأعِد تثبيت التطبيق" -} + "releases-to-this-repo-are-done-1-2-days-later": "يتم إصدار الإصدارات إلى هذا المستودع بعد يوم أو يومين" +} \ No newline at end of file diff --git a/website/langs/de.json b/website/langs/de.json index 48f4e87c46..ced2abffd3 100644 --- a/website/langs/de.json +++ b/website/langs/de.json @@ -233,18 +233,14 @@ "guide-dropdown-3": "Geheime Gruppen", "docs-dropdown-7": "SimpleX Chat übersetzen", "glossary": "Glossar", - "scan-the-qr-code-or": "Scannen Sie den QR-Code oder", - "use-this-url": "nutzen Sie diese URL", "signing-key-fingerprint": "Fingerabdruck des Signaturschlüssels (SHA-256)", "f-droid-org-repo": "F-Droid.org Repository", "stable-versions-built-by-f-droid-org": "Von F-Droid.org erstellte stabile Versionen", - "export": "die Chat-Datenbank exportieren", - "simplex-and-f-droid-repo-sign-builds-with-different-keys": "SimpleX Chat- und F-Droid.org-Repositorys signieren ihre Builds mit verschiedenen Schlüsseln. Zum Umschalten bitte", + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat- und F-Droid.org-Repositorys signieren ihre Builds mit verschiedenen Schlüsseln. Zum Umschalten bitte die Chat-Datenbank exportieren und die App neu installieren.", "releases-to-this-repo-are-done-1-2-days-later": "Die Versionen für dieses Repository werden 1..2 Tage später erstellt", - "chat-db-and-re-install-app": "und die App neu installieren", "docs-dropdown-8": "SimpleX Verzeichnisdienst", "simplex-chat-via-f-droid": "SimpleX Chat per F-Droid", "simplex-chat-repo": "SimpleX Chat Repository", "stable-and-beta-versions-built-by-developers": "Von den Entwicklern erstellte stabile und Beta-Versionen", - "to-add-it-to-your-f-droid-client": "Um es Ihrem F-Droid-Client hinzuzufügen" -} + "f-droid-page-simplex-chat-repo-section-text": "Um es Ihrem F-Droid-Client hinzuzufügen scannen Sie den QR-Code oder nutzen Sie diese URL:" +} \ No newline at end of file diff --git a/website/langs/en.json b/website/langs/en.json index 66989ef93d..4bb9036d69 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -237,14 +237,10 @@ "simplex-chat-via-f-droid": "SimpleX Chat via F-Droid", "simplex-chat-repo": "SimpleX Chat repo", "stable-and-beta-versions-built-by-developers": "Stable and beta versions built by the developers", - "to-add-it-to-your-f-droid-client": "To add it to your F-Droid client", - "scan-the-qr-code-or": "scan the QR code or", - "use-this-url": "use this URL", + "f-droid-page-simplex-chat-repo-section-text": "To add it to your F-Droid client, scan the QR code or use this URL:", "signing-key-fingerprint": "Signing key fingerprint (SHA-256)", "f-droid-org-repo": "F-Droid.org repo", "stable-versions-built-by-f-droid-org": "Stable versions built by F-Droid.org", "releases-to-this-repo-are-done-1-2-days-later": "The releases to this repo are done 1-2 days later", - "simplex-and-f-droid-repo-sign-builds-with-different-keys": "SimpleX Chat and F-Droid.org repositories sign builds with the different keys. To switch, please", - "export": "export", - "chat-db-and-re-install-app": "the chat database and re-install the app" + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat and F-Droid.org repositories sign builds with the different keys. To switch, please export the chat database and re-install the app." } diff --git a/website/langs/es.json b/website/langs/es.json index c3d20709df..4505d4b987 100644 --- a/website/langs/es.json +++ b/website/langs/es.json @@ -234,17 +234,13 @@ "back-to-top": "Volver arriba", "glossary": "Glosario", "stable-and-beta-versions-built-by-developers": "Versiones estables y beta compilados por los desarrolladores", - "to-add-it-to-your-f-droid-client": "Para añadirlo a tu cliente F-Droid", - "use-this-url": "usa esta URL", - "scan-the-qr-code-or": "escanea el código QR", - "export": "exportar", - "chat-db-and-re-install-app": "la base de datos y reinstala la aplicación", + "f-droid-page-simplex-chat-repo-section-text": "Para añadirlo a tu cliente F-Droid escanea el código QR usa esta URL:", "docs-dropdown-8": "Servicio Simplex Directory", "simplex-chat-repo": "Repositorio Simplex Chat", "simplex-chat-via-f-droid": "SimpleX Chat en F-Droid", "f-droid-org-repo": "Repositorio F-Droid.org", "stable-versions-built-by-f-droid-org": "Versión estable compilada por F-Droid.org", - "simplex-and-f-droid-repo-sign-builds-with-different-keys": "Los repositorios de SimpleX Chat y F-Droid.org firman con distinto certificado. Para cambiar, por favor", + "f-droid-page-f-droid-org-repo-section-text": "Los repositorios de SimpleX Chat y F-Droid.org firman con distinto certificado. Para cambiar, por favor exportar la base de datos y reinstala la aplicación.", "signing-key-fingerprint": "Huella digital de la clave de firma (SHA-256)", "releases-to-this-repo-are-done-1-2-days-later": "Las versiones aparecen 1-2 días más tarde en este repositorio" -} +} \ No newline at end of file diff --git a/website/langs/fr.json b/website/langs/fr.json index 5a87062cf0..ab29ca7c61 100644 --- a/website/langs/fr.json +++ b/website/langs/fr.json @@ -235,17 +235,13 @@ "on-this-page": "Sur cette page", "glossary": "Glossaire", "releases-to-this-repo-are-done-1-2-days-later": "Les mises à jour de ce dépôt sont faites 1 à 2 jours plus tard", - "simplex-and-f-droid-repo-sign-builds-with-different-keys": "Les dépôts SimpleX Chat et F-Droid.org signent les builds avec des clés différentes. Pour changer, veuillez", - "chat-db-and-re-install-app": "la base de données des chats et réinstaller l'application", - "export": "exporter", + "f-droid-page-f-droid-org-repo-section-text": "Les dépôts SimpleX Chat et F-Droid.org signent les builds avec des clés différentes. Pour changer, veuillez exporter la base de données des chats et réinstaller l'application.", "docs-dropdown-8": "Service de répertoire SimpleX", "simplex-chat-via-f-droid": "SimpleX Chat via F-Droid", "simplex-chat-repo": "Dépot SimpleX Chat", "stable-and-beta-versions-built-by-developers": "Versions stables et bêta crées par les développeurs", - "to-add-it-to-your-f-droid-client": "Pour l'ajouter à votre client F-Droid", - "scan-the-qr-code-or": "scannez le code QR ou", - "use-this-url": "utilisez cette URL", + "f-droid-page-simplex-chat-repo-section-text": "Pour l'ajouter à votre client F-Droid scannez le code QR ou utilisez cette URL:", "signing-key-fingerprint": "Empreinte de signature numérique (SHA-256)", "f-droid-org-repo": "Dépot F-Droid.org", "stable-versions-built-by-f-droid-org": "Versions stables créées par F-Droid.org" -} +} \ No newline at end of file diff --git a/website/langs/it.json b/website/langs/it.json index d7e9ca34db..41ea654b24 100644 --- a/website/langs/it.json +++ b/website/langs/it.json @@ -234,10 +234,7 @@ "docs-dropdown-7": "Traduci SimpleX Chat", "glossary": "Glossario", "releases-to-this-repo-are-done-1-2-days-later": "Le pubblicazioni su questo repo avvengono 1-2 giorni dopo", - "export": "esporta", - "simplex-and-f-droid-repo-sign-builds-with-different-keys": "I repository di SimpleX Chat e F-Droid.org firmano i pacchetti con chiavi diverse. Per passare da uno all'altro,", - "chat-db-and-re-install-app": "il database della chat e reinstalla l'app", - "use-this-url": "usa questo URL", + "f-droid-page-f-droid-org-repo-section-text": "I repository di SimpleX Chat e F-Droid.org firmano i pacchetti con chiavi diverse. Per passare da uno all'altro, esporta il database della chat e reinstalla l'app.", "signing-key-fingerprint": "Impronta della chiave di firma (SHA-256)", "f-droid-org-repo": "Repo di F-Droid.org", "stable-versions-built-by-f-droid-org": "Versioni stabili compilate da F-Droid.org", @@ -245,6 +242,5 @@ "simplex-chat-via-f-droid": "SimpleX Chat via F-Droid", "simplex-chat-repo": "Repo di SimpleX Chat", "stable-and-beta-versions-built-by-developers": "Versioni stabili e beta compilate dagli sviluppatori", - "to-add-it-to-your-f-droid-client": "Per aggiungerlo al tuo client F-Droid", - "scan-the-qr-code-or": "scansiona il codice QR o" -} + "f-droid-page-simplex-chat-repo-section-text": "Per aggiungerlo al tuo client F-Droid scansiona il codice QR o usa questo URL:" +} \ No newline at end of file diff --git a/website/langs/nl.json b/website/langs/nl.json index 71752b8bcd..980d16c0ff 100644 --- a/website/langs/nl.json +++ b/website/langs/nl.json @@ -236,15 +236,11 @@ "simplex-chat-via-f-droid": "SimpleX Chat via F-Droid", "simplex-chat-repo": "SimpleX Chat repo", "stable-and-beta-versions-built-by-developers": "Stabiele en bètaversies gebouwd door de ontwikkelaars", - "to-add-it-to-your-f-droid-client": "Om het toe te voegen aan uw F-Droid-client", - "scan-the-qr-code-or": "scan de QR-code of", - "use-this-url": "gebruik deze URL", + "f-droid-page-simplex-chat-repo-section-text": "Om het toe te voegen aan uw F-Droid-client scan de QR-code of gebruik deze URL:", "f-droid-org-repo": "F-Droid.org repo", "signing-key-fingerprint": "Signing key fingerprint (SHA-256)", "stable-versions-built-by-f-droid-org": "Stabiele versies gebouwd door F-Droid.org", "releases-to-this-repo-are-done-1-2-days-later": "De releases voor deze repository vinden 1-2 dagen later plaats", - "simplex-and-f-droid-repo-sign-builds-with-different-keys": "SimpleX Chat- en F-Droid.org-repository's ondertekenen builds met de verschillende sleutels. Om over te stappen, alstublieft", - "export": "exporteer", - "chat-db-and-re-install-app": "de chatdatabase en installeer de app opnieuw", + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat- en F-Droid.org-repository's ondertekenen builds met de verschillende sleutels. Om over te stappen, alstublieft exporteer de chatdatabase en installeer de app opnieuw.", "docs-dropdown-8": "SimpleX Directory Service" -} +} \ No newline at end of file diff --git a/website/langs/uk.json b/website/langs/uk.json index 7289e3be17..eae508ed47 100644 --- a/website/langs/uk.json +++ b/website/langs/uk.json @@ -234,8 +234,7 @@ "guide-dropdown-3": "Таємні групи", "glossary": "Глосарій", "docs-dropdown-8": "Служба каталогів SimpleX", - "to-add-it-to-your-f-droid-client": "Щоб додати його в клієнт F-Droid", - "scan-the-qr-code-or": "відскануйте QR-код або", + "f-droid-page-simplex-chat-repo-section-text": "Щоб додати його в клієнт F-Droid відскануйте QR-код або використовуйте цю URL-адресу:", "simplex-chat-via-f-droid": "SimpleX Chat через F-Droid", "signing-key-fingerprint": "Відбиток ключа підпису (SHA-256)", "stable-versions-built-by-f-droid-org": "Стабільні версії, зібрані на F-Droid.org", @@ -243,8 +242,5 @@ "f-droid-org-repo": "Репо F-Droid.org", "releases-to-this-repo-are-done-1-2-days-later": "Релізи в це репо відбуваються на 1-2 дні пізніше", "stable-and-beta-versions-built-by-developers": "Стабільні та бета-версії, створені розробниками", - "use-this-url": "використовуйте цю URL-адресу", - "simplex-and-f-droid-repo-sign-builds-with-different-keys": "Репозиторії SimpleX Chat та F-Droid.org підписують збірки з різними ключами. Щоб перемикнутися, будь ласка", - "export": "експорт", - "chat-db-and-re-install-app": "базу даних чату та перевстановіть додаток" -} + "f-droid-page-f-droid-org-repo-section-text": "Репозиторії SimpleX Chat та F-Droid.org підписують збірки з різними ключами. Щоб перемикнутися, будь ласка експорт базу даних чату та перевстановіть додаток." +} \ No newline at end of file diff --git a/website/src/_includes/contact_page.html b/website/src/_includes/contact_page.html index 9e81a0b50a..a03a3aea4f 100644 --- a/website/src/_includes/contact_page.html +++ b/website/src/_includes/contact_page.html @@ -1,3 +1,4 @@ +{% set lang = page.url | getlang %} {% block js_scripts %} @@ -18,7 +19,7 @@
- +
@@ -51,7 +52,7 @@
- +
diff --git a/website/src/_includes/hero.html b/website/src/_includes/hero.html index 43fb4a3541..161e0da4a8 100644 --- a/website/src/_includes/hero.html +++ b/website/src/_includes/hero.html @@ -1,3 +1,4 @@ +{% set lang = page.url | getlang %}
@@ -46,7 +47,7 @@
- +
diff --git a/website/src/_includes/sections/join_simplex.html b/website/src/_includes/sections/join_simplex.html index 400a2482df..776ec33d45 100644 --- a/website/src/_includes/sections/join_simplex.html +++ b/website/src/_includes/sections/join_simplex.html @@ -1,3 +1,5 @@ +{% set lang = page.url | getlang %} + {# join simplex #}
@@ -28,7 +30,7 @@
- +
diff --git a/website/src/fdroid.html b/website/src/fdroid.html index 9951a2d1e3..ff56b9cc95 100644 --- a/website/src/fdroid.html +++ b/website/src/fdroid.html @@ -5,7 +5,18 @@ description: "Get the app via F-Droid" templateEngineOverride: njk --- -
+ + +

{{ "simplex-chat-via-f-droid" | i18n({}, lang ) | safe }}

@@ -16,7 +27,7 @@ templateEngineOverride: njk

{{ "stable-and-beta-versions-built-by-developers" | i18n({}, lang ) | safe }}

-

{{ "to-add-it-to-your-f-droid-client" | i18n({}, lang ) | safe }}, {{ "use-this-url" | i18n({}, lang ) | safe }}:

+

{{ "f-droid-page-simplex-chat-repo-section-text" | i18n({}, lang ) | safe }}

https://app.simplex.chat/fdroid/repo @@ -40,7 +51,7 @@ templateEngineOverride: njk

{{ "stable-versions-built-by-f-droid-org" | i18n({}, lang ) | safe }}

{{ "releases-to-this-repo-are-done-1-2-days-later" | i18n({}, lang ) | safe }}.

-

{{ "simplex-and-f-droid-repo-sign-builds-with-different-keys" | i18n({}, lang ) | safe }} {{ "export" | i18n({}, lang ) | safe }} {{ "chat-db-and-re-install-app" | i18n({}, lang ) | safe }}.

+

{{ "f-droid-page-f-droid-org-repo-section-text" | i18n({}, lang ) | safe }}

diff --git a/website/web.sh b/website/web.sh index 9e3ae76ad6..e64f74d21d 100755 --- a/website/web.sh +++ b/website/web.sh @@ -32,6 +32,7 @@ for lang in "${langs[@]}"; do cp src/index.html src/$lang cp src/contact.html src/$lang cp src/invitation.html src/$lang + cp src/fdroid.html src/$lang echo "{\"lang\":\"$lang\"}" > src/$lang/$lang.json echo "done $lang copying" done From 95d57bc4e151cec5d400de1f27e024d53dd67b71 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sun, 27 Aug 2023 11:16:29 +0300 Subject: [PATCH 4/8] desktop: fixed gradle (#2982) * fix gradle * correct cert identity * proper file paths * moving to secrets * order of lines * returned back --- .github/workflows/build.yml | 4 ++++ apps/multiplatform/build.gradle.kts | 12 +++++++----- scripts/desktop/build-desktop-mac-ci.sh | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 09412eee55..d7322cd92f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -166,6 +166,10 @@ jobs: id: mac_desktop_build if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest' shell: bash + env: + APPLE_SIMPLEX_SIGNING_KEYCHAIN: ${{ secrets.APPLE_SIMPLEX_SIGNING_KEYCHAIN }} + APPLE_SIMPLEX_NOTARIZATION_APPLE_ID: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_APPLE_ID }} + APPLE_SIMPLEX_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_PASSWORD }} run: | scripts/desktop/build-desktop-mac-ci.sh echo "::set-output name=package_path::$(echo $PWD/release/main/dmg/SimpleX-*.dmg)" diff --git a/apps/multiplatform/build.gradle.kts b/apps/multiplatform/build.gradle.kts index 5e0ae5eb47..f277da4bde 100644 --- a/apps/multiplatform/build.gradle.kts +++ b/apps/multiplatform/build.gradle.kts @@ -9,6 +9,8 @@ buildscript { // No file was created } } + fun ExtraPropertiesExtension.getOrNull(name: String): Any? = if (has(name)) get("name") else null + extra.set("compose.version", prop["compose.version"] ?: extra["compose.version"]) extra.set("kotlin.version", prop["kotlin.version"] ?: extra["kotlin.version"]) extra.set("gradle.plugin.version", prop["gradle.plugin.version"] ?: extra["gradle.plugin.version"]) @@ -30,11 +32,11 @@ buildscript { /** Mac signing and notarization */ // You can specify `compose.desktop.mac.*` keys and values from the right side of the command in `$HOME/.gradle/gradle.properties`. // This will be project-independent setup without requiring to have `local.properties` file - extra.set("desktop.mac.signing.identity", prop["desktop.mac.signing.identity"] ?: extra["compose.desktop.mac.signing.identity"]) - extra.set("desktop.mac.signing.keychain", prop["desktop.mac.signing.keychain"] ?: extra["compose.desktop.mac.signing.keychain"]) - extra.set("desktop.mac.notarization.apple_id", prop["desktop.mac.notarization.apple_id"] ?: extra["compose.desktop.mac.notarization.appleID"]) - extra.set("desktop.mac.notarization.password", prop["desktop.mac.notarization.password"] ?: extra["compose.desktop.mac.notarization.password"]) - extra.set("desktop.mac.notarization.team_id", prop["desktop.mac.notarization.team_id"] ?: extra["compose.desktop.mac.notarization.ascProvider"]) + extra.set("desktop.mac.signing.identity", prop["desktop.mac.signing.identity"] ?: extra.getOrNull("compose.desktop.mac.signing.identity")) + extra.set("desktop.mac.signing.keychain", prop["desktop.mac.signing.keychain"] ?: extra.getOrNull("compose.desktop.mac.signing.keychain")) + extra.set("desktop.mac.notarization.apple_id", prop["desktop.mac.notarization.apple_id"] ?: extra.getOrNull("compose.desktop.mac.notarization.appleID")) + extra.set("desktop.mac.notarization.password", prop["desktop.mac.notarization.password"] ?: extra.getOrNull("compose.desktop.mac.notarization.password")) + extra.set("desktop.mac.notarization.team_id", prop["desktop.mac.notarization.team_id"] ?: extra.getOrNull("compose.desktop.mac.notarization.ascProvider")) repositories { google() diff --git a/scripts/desktop/build-desktop-mac-ci.sh b/scripts/desktop/build-desktop-mac-ci.sh index d11dfccb58..07a3db9c8e 100755 --- a/scripts/desktop/build-desktop-mac-ci.sh +++ b/scripts/desktop/build-desktop-mac-ci.sh @@ -2,7 +2,7 @@ set -e -trap "rm apps/multiplatform/local.properties; rm /tmp/simplex.keychain" EXIT +trap "rm apps/multiplatform/local.properties || true; rm local.properties || true; rm /tmp/simplex.keychain || true" EXIT echo "desktop.mac.signing.identity=Developer ID Application: SimpleX Chat Ltd (5NN7GUYB6T)" >> apps/multiplatform/local.properties echo "desktop.mac.signing.keychain=/tmp/simplex.keychain" >> apps/multiplatform/local.properties echo "desktop.mac.notarization.apple_id=$APPLE_SIMPLEX_NOTARIZATION_APPLE_ID" >> apps/multiplatform/local.properties From 0f076d9ac91a7fec5984614dfec6a55c30f474c6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 28 Aug 2023 20:32:44 +0100 Subject: [PATCH 5/8] docs: update technical details and limitations --- README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 56e219d50a..561a322c62 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ SimpleX Chat founder - [News and updates](#news-and-updates) - [Quick installation of a terminal app](#zap-quick-installation-of-a-terminal-app) - [SimpleX Platform design](#simplex-platform-design) -- [Privacy: technical details and limitations](#privacy-technical-details-and-limitations) +- [Privacy and security: technical details and limitations](#privacy-and-security-technical-details-and-limitations) - [For developers](#for-developers) - [Roadmap](#roadmap) - [Disclaimers, Security contact, License](#disclaimers) @@ -262,7 +262,7 @@ See [SimpleX whitepaper](https://github.com/simplex-chat/simplexmq/blob/stable/p See [SimpleX Chat Protocol](./docs/protocol/simplex-chat.md) for the format of messages sent between chat clients over [SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md). -## Privacy: technical details and limitations +## Privacy and security: technical details and limitations SimpleX Chat is a work in progress – we are releasing improvements as they are ready. You have to decide if the current state is good enough for your usage scenario. @@ -281,12 +281,15 @@ What is already implemented: 9. To protect your IP address all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details. 10. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings. 11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections. +12. Manual messaging queue rotations to move conversation to another SMP relay. -We plan to add soon: +We plan to add: -1. Automatic message queue rotation. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days). -2. Local files encryption. Currently the images and files you send and receive are stored in the app unencrypted, you can delete them via `Settings / Database passphrase & export`. -3. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time. +1. Local files encryption. Currently the images and files you send and receive are stored in the app unencrypted, you can delete them via `Settings / Database passphrase & export`. This is currently in progress. +2. Senders' SMP relays and recipients' XFTP relays to reduce traffic and conceal IP addresses from the relays chosen, and potentially controlled, by another party. +3. Automatic message queue rotation and redundancy. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days). +4. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time. +5. Reproducible builds – this is the limitation of the development stack, but we will be investing into solving this problem. Users can still build all applications and services from the source code. ## For developers From 4aac3c7922c9ec7ba0c5641317337341f7081d06 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 28 Aug 2023 22:23:49 +0100 Subject: [PATCH 6/8] website: update comparison --- website/langs/en.json | 5 +++-- website/src/index.html | 11 ++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/website/langs/en.json b/website/langs/en.json index 4bb9036d69..d9ff80f3e4 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -205,8 +205,9 @@ "comparison-section-list-point-1": "Usually based on a phone number, in some cases on usernames", "comparison-section-list-point-2": "DNS-based addresses", "comparison-section-list-point-3": "Public key or some other globally unique ID", - "comparison-section-list-point-4": "If operator’s servers are compromised", - "comparison-section-list-point-5": "Does not protect users' metadata", + "comparison-section-list-point-4a": "SimpleX relays cannot compromise e2e encryption. Verify security code to mitigate attack on out-of-band channel", + "comparison-section-list-point-4": "If operator’s servers are compromised. Verify security code in Signal and some other apps to mitigate it", + "comparison-section-list-point-5": "Does not protect users' metadata privacy", "comparison-section-list-point-6": "While P2P are distributed, they are not federated - they operate as a single network", "comparison-section-list-point-7": "P2P networks either have a central authority or the whole network can be compromised", "see-here": "see here", diff --git a/website/src/index.html b/website/src/index.html index 16bab8d98f..f1bde906a2 100644 --- a/website/src/index.html +++ b/website/src/index.html @@ -189,8 +189,8 @@ active_home: true {{ "comparison-point-2-text" | i18n({}, lang ) | safe }} - {{ "no-secure" | i18n({}, lang ) | safe }} - {{ "yes" | i18n({}, lang ) | safe }} 4 + {{ "no-secure" | i18n({}, lang ) | safe }} 4 + {{ "yes" | i18n({}, lang ) | safe }} 5 {{ "yes" | i18n({}, lang ) | safe }} {{ "yes" | i18n({}, lang ) | safe }} @@ -205,15 +205,15 @@ active_home: true {{ "comparison-point-4-text" | i18n({}, lang ) | safe }} {{ "no-decentralized" | i18n({}, lang ) | safe }} {{ "yes" | i18n({}, lang ) | safe }} - {{ "no-federated" | i18n({}, lang ) | safe }} 5 - {{ "yes" | i18n({}, lang ) | safe }} 6 + {{ "no-federated" | i18n({}, lang ) | safe }} 6 + {{ "yes" | i18n({}, lang ) | safe }} 7 {{ "comparison-point-5-text" | i18n({}, lang ) | safe }} {{ "no-resilient" | i18n({}, lang ) | safe }} {{ "yes" | i18n({}, lang ) | safe }} {{ "yes" | i18n({}, lang ) | safe }} 2 - {{ "yes" | i18n({}, lang ) | safe }} 7 + {{ "yes" | i18n({}, lang ) | safe }} 8 @@ -227,6 +227,7 @@ active_home: true
  • {{ "comparison-section-list-point-1" | i18n({}, lang ) | safe }}
  • {{ "comparison-section-list-point-2" | i18n({}, lang ) | safe }}
  • {{ "comparison-section-list-point-3" | i18n({}, lang ) | safe }}
  • +
  • {{ "comparison-section-list-point-4a" | i18n({}, lang ) | safe }}
  • {{ "comparison-section-list-point-4" | i18n({}, lang ) | safe }}
  • {{ "comparison-section-list-point-5" | i18n({}, lang ) | safe }}
  • {{ "comparison-section-list-point-6" | i18n({}, lang ) | safe }}
  • From 1c90eb0a2ead5b47d7bb4494233ee863131ab19b Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 1 Sep 2023 19:43:07 +0400 Subject: [PATCH 7/8] docs: groups improvements rfc (#2988) --- docs/rfcs/2023-08-28-groups-improvements.md | 112 ++++++++++++++++++ .../2023-08-28-groups-improvements.mmd | 40 +++++++ .../2023-08-28-groups-improvements.svg | 1 + 3 files changed, 153 insertions(+) create mode 100644 docs/rfcs/2023-08-28-groups-improvements.md create mode 100644 docs/rfcs/diagrams/2023-08-28-groups-improvements.mmd create mode 100644 docs/rfcs/diagrams/2023-08-28-groups-improvements.svg diff --git a/docs/rfcs/2023-08-28-groups-improvements.md b/docs/rfcs/2023-08-28-groups-improvements.md new file mode 100644 index 0000000000..7a653fcb26 --- /dev/null +++ b/docs/rfcs/2023-08-28-groups-improvements.md @@ -0,0 +1,112 @@ +# Groups improvements + +See also: +- [Group contacts management](./2022-10-19-group-contacts-management.md). +- [Create groups without establishing direct connections](./2023-08-10-groups-wt-contacts.md). + +## Problem + +Establishing connections in groups is unstable and uses a lot of traffic. There are several areas for improvement that that could help optimize it: + +- Joining group member prematurely creates direct and group connections for each member. + + Some members may never come online, and that traffic would be completely wasted. + + Instead of creating direct connections, we could allow to send direct messages inside group, and optionally have a separate protocol for automating establishing direct connection with member via them. + +- Host sends N introduction messages (XGrpMemIntro) to joining member. Instead they could be batched. + +## Possible solutions + +### Improved group handshake protocol + +Below are proposed changes to group handshake protocol to reduce traffic and improve stability. + +Each joining member creates a new temporary per group address for introduced members to connect via. Joining member sends it to host when accepting group invitation. + +``` haskell +XGrpAcptAddress :: MemberId -> ConnReqContact -> ChatMsgEvent 'Json +``` + +Host sends group introductions in batches, batching smaller messages first (introductions of members without profile picture). + +For each received batch of N introductions joining member creates N transient per member identifiers (MemberCodes) and replies to host with batched XGrpMemInv messages including these identifiers. Joining member would then use them to verify contact requests from introduced members. + +How is MemberCode different from MemberId? - MemberId is known to all group members and is constant per member per group. MemberCode would be known only to host and to introduced member (of existing members), so other members wouldn't be able to impersonate one another when requesting connection with joining member. An introduced member can still pass their identifier + joining member address to another member or outside of group, but it is no different to passing currently shared invitation links. + +```haskell +newtype MemberCode = MemberCode {unMemberCode :: ByteString} + +XGrpMemInvCode :: MemberId -> MemberCode -> ChatMsgEvent 'Json + +-- instead of / in addition to batching message could be + +type MemberCodes = Map MemberId MemberCode + +XGrpMemInvCodes :: MemberCodes -> ChatMsgEvent 'Json +``` + +Host includes joining member address and code (unique for each introduced member) into XGrpMemFwd messages instead of invitation links: + +```haskell +XGrpMemFwdCode :: MemberInfo -> ConnReqContact -> MemberCode -> ChatMsgEvent 'Json +``` + +Introduced members send contact requests with a new message XGroupMember / XIntroduced (similar to XInfo or XContact, see `processUserContactRequest`): + +```haskell +XIntroduced :: MemberInfo -> MemberCode -> ChatMsgEvent 'Json +``` + +Joinee verifies profile and code and automatically accepts contact request. They both assign resulting connection to respective group member record, without creating contact. + +After (if) all introduced members have connected, joining member deletes per group address. Possibly it can also be deleted after expiration interval. + +#### Group links + +We can reduce number of steps taken to join group via group link: +- Do not create direct connection and contact with group link host, instead use the connection resulting from contact request as a group connection, and assign it to a group member record. +- Host to not send XGrpInv message, joining member to not wait for it, instead joining member would initiate with XGrpAcptAddress after establishing connection via group link. + +In addition to their profile, host includes MemberId of joining member into confirmation when accepting group link join request, using new message: + +```haskell +XGroupLinkInfo :: Profile -> MemberId -> ChatMsgEvent 'Json +``` + +Joining member initially doesn't know group profile, they create a placeholder group with a new dummy profile (alternatively, we could include at least group display name into group link). After connection is established, host sends XGrpInfo containing group profile to joining member. This can happen in parallel with group handshake started by XGrpAcptAddress. + +Group profile could also be included into XGroupLinkInfo if not for the limitation on size if both host's profile and group profile contain pictures. + +![Adding member to the group](./diagrams/2023-08-28-groups-improvements.svg) + +#### Clients compatibility + +We have a [proposed mechanism](https://github.com/simplex-chat/simplex-chat/pull/2886) for communicating "chat protocol version" between clients. + +Sending and processing new protocol messages would only be supported by updated clients. + +Trying to support both protocols across different members in the same group would require complex logic: + +Host would have to send introduced members versions, joining member would provide both address or invitation links depending on each members' versions, host would forward accordingly. + +Instead we could assign "chat protocol version" per group and share it with members as part of group profile, and make a two-stage release when members would first be able to update and get new processing logic, but have it disabled until next release. + +After group switching to new processing logic old clients wouldn't be able to connect in groups. + +How should existing groups be switched? +- Owner user action? +- Owner client deciding automatically? +- In case group has multiple owners - which owner(s) can / should decide? +- Prohibited until all / part of existing members don't update? How to request members to update? +- Old clients will not be able to process and save group chat version from group profile update. + +### Sending direct messages inside group + +Group messages are sent by broadcasting them to all group member connections. As a replacement for creating additional direct connections in group we can allow to send message directly to members via group member connections. The UX would be to choose whether to send to group or to a specific member via compose view. + +Possible approach is to extend ExtMsgContent with `direct :: Maybe Bool` field, which would only be considered for group messages. + +Chat items should store information of receiving member database ID (for sending member) and of message being direct (for receiving member). Perhaps it could be a single field `direct_member_id`, which would be the same as `group_member_id` for received messages. + +TODO - consider whether `connection_id` or `group_id` or both should be assigned in `messages` table. diff --git a/docs/rfcs/diagrams/2023-08-28-groups-improvements.mmd b/docs/rfcs/diagrams/2023-08-28-groups-improvements.mmd new file mode 100644 index 0000000000..591c30445e --- /dev/null +++ b/docs/rfcs/diagrams/2023-08-28-groups-improvements.mmd @@ -0,0 +1,40 @@ +sequenceDiagram + participant M as N existing
    members + participant A as Alice + participant B as Bob + + note over A, B: 1. send and accept group invitation /
    join via group link + alt host invites contact + A ->> B: x.grp.inv
    invite Bob to group
    (via contact connection) + else user joins via group link + B ->> A: request to join group via link + A ->> B: auto-accept
    x.group.link.info with host's profile
    and joining member MemberId
    establish group member connection + A ->> B: x.grp.info
    group profile + end + + note right of B: when joining via group link
    Bob doesn't wait for x.grp.info
    and initiates group handshake
    with x.grp.acpt.address
    after establishing connection + + note over B: create per group address + B ->> A: x.grp.acpt.address
    accept invitation
    and send address to connect
    (via member connection) + B ->> A: establish group member connection + + note over M, B: 2. introduce new member Bob to all existing members + A ->> M: x.grp.mem.new
    "announce" Bob
    to existing members
    (via member connections) + + loop batched + A ->> B: x.grp.mem.intro * N
    "introduce" members
    (via member connection) + note over B: create N MemberCodes + B ->> A: x.grp.mem.inv.code
    unique MemberCodes
    for all members
    (via member connection) + end + + A ->> M: x.grp.mem.fwd.code
    forward address
    and unique MemberCodes
    to all members
    (via member connections) + + note over M, B: 3. establish group member connection + M ->> B: request group member connection
    x.introduced with MemberCode + B ->> M: verify MemberCode, auto-accept + + note over M, B: no contact deduplication + + opt all introduced members connected / expiration + note over B: delete per group address + end diff --git a/docs/rfcs/diagrams/2023-08-28-groups-improvements.svg b/docs/rfcs/diagrams/2023-08-28-groups-improvements.svg new file mode 100644 index 0000000000..34529d5329 --- /dev/null +++ b/docs/rfcs/diagrams/2023-08-28-groups-improvements.svg @@ -0,0 +1 @@ +BobAliceN existingmembersBobAliceN existingmembers1. send and accept group invitation /join via group linkalt[host invites contact][user joins via group link]when joining via group linkBob doesn't wait for x.grp.infoand initiates group handshakewith x.grp.acpt.addressafter establishing connectioncreate per group address2. introduce new member Bob to all existing memberscreate N MemberCodesloop[batched]3. establish group member connectionno contact deduplicationdelete per group addressopt[all introducedmembersconnected /expiration]x.grp.invinvite Bob to group(via contact connection)request to join group via linkauto-acceptx.group.link.info with host's profileand joining member MemberIdestablish group member connectionx.grp.infogroup profilex.grp.acpt.addressaccept invitationand send address to connect(via member connection)establish group member connectionx.grp.mem.new"announce" Bobto existing members(via member connections)x.grp.mem.intro * N"introduce" members(via member connection)x.grp.mem.inv.codeunique MemberCodesfor all members(via member connection)x.grp.mem.fwd.codeforward addressand unique MemberCodesto all members(via member connections)request group member connectionx.introduced with MemberCodeverify MemberCode, auto-accept \ No newline at end of file From 0b214acf97931994d85f1efbe442ad984330a9da Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 1 Sep 2023 19:43:27 +0100 Subject: [PATCH 8/8] core: support encrypted local files (#2989) * core: support encrypted local files * add migration * update agent api, chat api * fix query, exported functions to read/write files * update simplexmq * remove formatting changes * test, fix file size * reduce diff Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> * fail when receiving SMP files with local encryption * update simplexmq * remove style changes --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 7 +- src/Simplex/Chat.hs | 125 ++++++++++-------- src/Simplex/Chat/Bot.hs | 2 +- src/Simplex/Chat/Controller.hs | 30 ++++- src/Simplex/Chat/Messages.hs | 12 +- src/Simplex/Chat/Messages/CIContent.hs | 2 +- .../Migrations/M20230827_file_encryption.hs | 20 +++ src/Simplex/Chat/Migrations/chat_schema.sql | 4 +- src/Simplex/Chat/Mobile.hs | 8 +- src/Simplex/Chat/Mobile/File.hs | 83 ++++++++++++ src/Simplex/Chat/Mobile/Shared.hs | 19 +++ src/Simplex/Chat/Mobile/WebRTC.hs | 17 +-- src/Simplex/Chat/Store/Files.hs | 60 ++++++--- src/Simplex/Chat/Store/Messages.hs | 37 +++--- src/Simplex/Chat/Store/Migrations.hs | 4 +- src/Simplex/Chat/Types.hs | 18 ++- src/Simplex/Chat/View.hs | 42 +++--- stack.yaml | 2 +- tests/ChatClient.hs | 2 +- tests/ChatTests/Files.hs | 39 +++++- 22 files changed, 390 insertions(+), 147 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20230827_file_encryption.hs create mode 100644 src/Simplex/Chat/Mobile/File.hs create mode 100644 src/Simplex/Chat/Mobile/Shared.hs diff --git a/cabal.project b/cabal.project index 1be6c7365b..5338f229a0 100644 --- a/cabal.project +++ b/cabal.project @@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 4c0b8a31d20870a23e120e243359901d8240f922 + tag: 5dc3d739b206edc2b4706ba0eef64ad4492e68e6 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index f3ad3061a6..4598c9c048 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."4c0b8a31d20870a23e120e243359901d8240f922" = "0lrgfm8di0x4rmidqp7k2fw29yaal6467nmb85lwk95yz602906z"; + "https://github.com/simplex-chat/simplexmq.git"."5dc3d739b206edc2b4706ba0eef64ad4492e68e6" = "0nzp0ijmw7ppmzjj72hf0b8jkyg8lwwy92hc1649xk3hnrj48wfz"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb"; "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index f26e3432c9..dbff343507 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -1,6 +1,6 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.35.0. +-- This file has been generated from package.yaml by hpack version 0.35.2. -- -- see: https://github.com/sol/hpack @@ -10,7 +10,7 @@ category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat maintainer: chat@simplex.chat -copyright: 2020-23 simplex.chat +copyright: 2020-22 simplex.chat license: AGPL-3 license-file: LICENSE build-type: Simple @@ -108,7 +108,10 @@ library Simplex.Chat.Migrations.M20230705_delivery_receipts Simplex.Chat.Migrations.M20230721_group_snd_item_statuses Simplex.Chat.Migrations.M20230814_indexes + Simplex.Chat.Migrations.M20230827_file_encryption Simplex.Chat.Mobile + Simplex.Chat.Mobile.File + Simplex.Chat.Mobile.Shared Simplex.Chat.Mobile.WebRTC Simplex.Chat.Options Simplex.Chat.ProfileGenerator diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 1fabed45b4..dd7e904253 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} @@ -86,6 +85,8 @@ import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import Simplex.Messaging.Client (defaultNetworkConfig) import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) +import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (base64P) @@ -562,8 +563,9 @@ processChatCommand = \case SendFileSMP fileInline -> smpSndFileTransfer file fileSize fileInline SendFileXFTP -> xftpSndFileTransfer user file fileSize 1 $ CGContact ct where - smpSndFileTransfer :: FilePath -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta) - smpSndFileTransfer file fileSize fileInline = do + smpSndFileTransfer :: CryptoFile -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta) + smpSndFileTransfer (CryptoFile _ (Just _)) _ _ = throwChatError $ CEFileInternal "locally encrypted files can't be sent via SMP" -- can only happen if XFTP is disabled + smpSndFileTransfer (CryptoFile file Nothing) fileSize fileInline = do (agentConnId_, fileConnReq) <- if isJust fileInline then pure (Nothing, Nothing) @@ -576,7 +578,8 @@ processChatCommand = \case fileStatus <- case fileInline of Just IFMSent -> createSndDirectInlineFT db ct ft $> CIFSSndTransfer 0 1 _ -> pure CIFSSndStored - let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus, fileProtocol = FPSMP} + let fileSource = Just $ CF.plain file + ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol = FPSMP} pure (fileInvitation, ciFile, ft) prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> m (MsgContainer, Maybe (CIQuote 'CTDirect)) prepareMsg fInv_ timed_ = case quotedItemId_ of @@ -625,15 +628,17 @@ processChatCommand = \case SendFileSMP fileInline -> smpSndFileTransfer file fileSize fileInline SendFileXFTP -> xftpSndFileTransfer user file fileSize n $ CGGroup g where - smpSndFileTransfer :: FilePath -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta) - smpSndFileTransfer file fileSize fileInline = do + smpSndFileTransfer :: CryptoFile -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta) + smpSndFileTransfer (CryptoFile _ (Just _)) _ _ = throwChatError $ CEFileInternal "locally encrypted files can't be sent via SMP" -- can only happen if XFTP is disabled + smpSndFileTransfer (CryptoFile file Nothing) fileSize fileInline = do let fileName = takeFileName file fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq = Nothing, fileInline, fileDescr = Nothing} fileStatus = if fileInline == Just IFMSent then CIFSSndTransfer 0 1 else CIFSSndStored chSize <- asks $ fileChunkSize . config withStore' $ \db -> do ft@FileTransferMeta {fileId} <- createSndGroupFileTransfer db userId gInfo file fileInvitation chSize - let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus, fileProtocol = FPSMP} + let fileSource = Just $ CF.plain file + ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol = FPSMP} pure (fileInvitation, ciFile, ft) sendGroupFileInline :: [GroupMember] -> SharedMsgId -> FileTransferMeta -> m () sendGroupFileInline ms sharedMsgId ft@FileTransferMeta {fileInline} = @@ -688,17 +693,19 @@ processChatCommand = \case qText = msgContentText qmc qFileName = maybe qText (T.pack . (fileName :: CIFile d -> String)) ciFile_ qTextOrFile = if T.null qText then qFileName else qText - xftpSndFileTransfer :: User -> FilePath -> Integer -> Int -> ContactOrGroup -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta) - xftpSndFileTransfer user file fileSize n contactOrGroup = do - let fileName = takeFileName file + xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta) + xftpSndFileTransfer user file@(CryptoFile filePath cfArgs) fileSize n contactOrGroup = do + let fileName = takeFileName filePath fileDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False} fInv = xftpFileInvitation fileName fileSize fileDescr - fsFilePath <- toFSFilePath file - aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) fsFilePath (roundedFDCount n) + fsFilePath <- toFSFilePath filePath + let srcFile = CryptoFile fsFilePath cfArgs + aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) srcFile (roundedFDCount n) -- TODO CRSndFileStart event for XFTP chSize <- asks $ fileChunkSize . config ft@FileTransferMeta {fileId} <- withStore' $ \db -> createSndFileTransferXFTP db user contactOrGroup file fInv (AgentSndFileId aFileId) chSize - let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored, fileProtocol = FPXFTP} + let fileSource = Just $ CryptoFile filePath cfArgs + ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus = CIFSSndStored, fileProtocol = FPXFTP} case contactOrGroup of CGContact Contact {activeConn} -> withStore' $ \db -> createSndFTDescrXFTP db user Nothing activeConn ft fileDescr CGGroup (Group _ ms) -> forM_ ms $ \m -> saveMemberFD m `catchChatError` (toView . CRChatError (Just user)) @@ -1613,26 +1620,40 @@ processChatCommand = \case asks showLiveItems >>= atomically . (`writeTVar` on) >> ok_ SendFile chatName f -> withUser $ \user -> do chatRef <- getChatRef user chatName - processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage (Just f) Nothing (MCFile "") + processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage (Just $ CF.plain f) Nothing (MCFile "") SendImage chatName f -> withUser $ \user -> do chatRef <- getChatRef user chatName filePath <- toFSFilePath f - unless (any ((`isSuffixOf` map toLower f)) imageExtensions) $ throwChatError CEFileImageType {filePath} + unless (any (`isSuffixOf` map toLower f) imageExtensions) $ throwChatError CEFileImageType {filePath} fileSize <- getFileSize filePath unless (fileSize <= maxImageSize) $ throwChatError CEFileImageSize {filePath} -- TODO include file description for preview - processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage (Just f) Nothing (MCImage "" fixedImagePreview) + processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage (Just $ CF.plain f) Nothing (MCImage "" fixedImagePreview) ForwardFile chatName fileId -> forwardFile chatName fileId SendFile ForwardImage chatName fileId -> forwardFile chatName fileId SendImage SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO" - ReceiveFile fileId rcvInline_ filePath_ -> withUser $ \_ -> + ReceiveFile fileId encrypted rcvInline_ filePath_ -> withUser $ \_ -> withChatLock "receiveFile" . procCmd $ do - (user, ft) <- withStore $ \db -> getRcvFileTransferById db fileId - receiveFile' user ft rcvInline_ filePath_ - SetFileToReceive fileId -> withUser $ \_ -> do + (user, ft) <- withStore (`getRcvFileTransferById` fileId) + ft' <- if encrypted then encryptLocalFile ft else pure ft + receiveFile' user ft' rcvInline_ filePath_ + where + encryptLocalFile ft@RcvFileTransfer {xftpRcvFile} = case xftpRcvFile of + Nothing -> throwChatError $ CEFileInternal "locally encrypted files can't be received via SMP" + Just f -> do + cfArgs <- liftIO $ CF.randomArgs + withStore' $ \db -> setFileCryptoArgs db fileId cfArgs + pure ft {xftpRcvFile = Just ((f :: XFTPRcvFile) {cryptoArgs = Just cfArgs})} + SetFileToReceive fileId encrypted -> withUser $ \_ -> do withChatLock "setFileToReceive" . procCmd $ do - withStore' (`setRcvFileToReceive` fileId) + cfArgs <- if encrypted then fileCryptoArgs else pure Nothing + withStore' $ \db -> setRcvFileToReceive db fileId cfArgs ok_ + where + fileCryptoArgs = do + (_, RcvFileTransfer {xftpRcvFile = f}) <- withStore (`getRcvFileTransferById` fileId) + unless (isJust f) $ throwChatError $ CEFileInternal "locally encrypted files can't be received via SMP" + liftIO $ Just <$> CF.randomArgs CancelFile fileId -> withUser $ \user@User {userId} -> withChatLock "cancelFile" . procCmd $ withStore (\db -> getFileTransfer db user fileId) >>= \case @@ -1829,18 +1850,19 @@ processChatCommand = \case contactMember Contact {contactId} = find $ \GroupMember {memberContactId = cId, memberStatus = s} -> cId == Just contactId && s /= GSMemRemoved && s /= GSMemLeft - checkSndFile :: MsgContent -> FilePath -> Integer -> m (Integer, SendFileMode) - checkSndFile mc f n = do + checkSndFile :: MsgContent -> CryptoFile -> Integer -> m (Integer, SendFileMode) + checkSndFile mc (CryptoFile f cfArgs) n = do fsFilePath <- toFSFilePath f unlessM (doesFileExist fsFilePath) . throwChatError $ CEFileNotFound f ChatConfig {fileChunkSize, inlineFiles} <- asks config xftpCfg <- readTVarIO =<< asks userXFTPFileConfig - fileSize <- getFileSize fsFilePath + fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cfArgs when (fromInteger fileSize > maxFileSize) $ throwChatError $ CEFileSize f - let chunks = - ((- fileSize) `div` fileChunkSize) + let chunks = -((-fileSize) `div` fileChunkSize) fileInline = inlineFileMode mc inlineFiles chunks n fileMode = case xftpCfg of Just cfg + | isJust cfArgs -> SendFileXFTP | fileInline == Just IFMSent || fileSize < minFileSize cfg || n <= 0 -> SendFileSMP fileInline | otherwise -> SendFileXFTP _ -> SendFileSMP fileInline @@ -1867,17 +1889,17 @@ processChatCommand = \case summary <- foldM (processAndCount user' logLevel) (UserProfileUpdateSummary 0 0 0 []) contacts pure $ CRUserProfileUpdated user' (fromLocalProfile p) p' summary where - processAndCount user' ll (!s@UserProfileUpdateSummary {notChanged, updateSuccesses, updateFailures, changedContacts = cts}) ct = do + processAndCount user' ll s@UserProfileUpdateSummary {notChanged, updateSuccesses, updateFailures, changedContacts = cts} ct = do let mergedProfile = userProfileToSend user Nothing $ Just ct ct' = updateMergedPreferences user' ct mergedProfile' = userProfileToSend user' Nothing $ Just ct' if mergedProfile' == mergedProfile then pure s {notChanged = notChanged + 1} - else - let cts' = if mergedPreferences ct == mergedPreferences ct' then cts else ct' : cts + else + let cts' = if mergedPreferences ct == mergedPreferences ct' then cts else ct' : cts in (notifyContact mergedProfile' ct' $> s {updateSuccesses = updateSuccesses + 1, changedContacts = cts'}) `catchChatError` \e -> when (ll <= CLLInfo) (toView $ CRChatError (Just user) e) $> s {updateFailures = updateFailures + 1, changedContacts = cts'} - where + where notifyContact mergedProfile' ct' = do void $ sendDirectContactMessage ct' (XInfo mergedProfile') when (directOrUsed ct') $ createSndFeatureItems user' ct ct' @@ -2214,7 +2236,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI filePath <- getRcvFilePath fileId filePath_ fName True withStoreCtx (Just "acceptFileReceive, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db user fileId connIds ConnJoined filePath -- XFTP - (Just _xftpRcvFile, _) -> do + (Just XFTPRcvFile {cryptoArgs}, _) -> do filePath <- getRcvFilePath fileId filePath_ fName False (ci, rfd) <- withStoreCtx (Just "acceptFileReceive, xftpAcceptRcvFT ...") $ \db -> do -- marking file as accepted and reading description in the same transaction @@ -2222,7 +2244,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI ci <- xftpAcceptRcvFT db user fileId filePath rfd <- getRcvFileDescrByFileId db fileId pure (ci, rfd) - receiveViaCompleteFD user fileId rfd + receiveViaCompleteFD user fileId rfd cryptoArgs pure ci -- group & direct file protocol _ -> do @@ -2265,11 +2287,11 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI || (rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks) ) -receiveViaCompleteFD :: ChatMonad m => User -> FileTransferId -> RcvFileDescr -> m () -receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} = +receiveViaCompleteFD :: ChatMonad m => User -> FileTransferId -> RcvFileDescr -> Maybe CryptoFileArgs -> m () +receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} cfArgs = when fileDescrComplete $ do rd <- parseFileDescription fileDescrText - aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd + aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs startReceivingFile user fileId withStoreCtx' (Just "receiveViaCompleteFD, updateRcvFileAgentId") $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId) @@ -2535,7 +2557,7 @@ cleanupManager = do `catchChatError` (toView . CRChatError (Just user)) cleanupMessages = do ts <- liftIO getCurrentTime - let cutoffTs = addUTCTime (- (30 * nominalDay)) ts + let cutoffTs = addUTCTime (-(30 * nominalDay)) ts withStoreCtx' (Just "cleanupManager, deleteOldMessages") (`deleteOldMessages` cutoffTs) startProximateTimedItemThread :: ChatMonad m => User -> (ChatRef, ChatItemId) -> UTCTime -> m () @@ -3567,14 +3589,14 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do processFDMessage fileId fileDescr = do ft <- withStore $ \db -> getRcvFileTransfer db user fileId unless (rcvFileCompleteOrCancelled ft) $ do - (rfd, RcvFileTransfer {fileStatus}) <- withStore $ \db -> do + (rfd, RcvFileTransfer {fileStatus, xftpRcvFile}) <- withStore $ \db -> do rfd <- appendRcvFD db userId fileId fileDescr -- reading second time in the same transaction as appending description -- to prevent race condition with accept ft' <- getRcvFileTransfer db user fileId pure (rfd, ft') - case fileStatus of - RFSAccepted _ -> receiveViaCompleteFD user fileId rfd + case (fileStatus, xftpRcvFile) of + (RFSAccepted _, Just XFTPRcvFile {cryptoArgs}) -> receiveViaCompleteFD user fileId rfd cryptoArgs _ -> pure () cancelMessageFile :: Contact -> SharedMsgId -> MsgMeta -> m () @@ -3600,7 +3622,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do withStore' $ \db -> startRcvInlineFT db user ft fPath inline pure (Just fPath, CIFSRcvAccepted) _ -> pure (Nothing, CIFSRcvInvitation) - pure (ft, CIFile {fileId, fileName, fileSize, filePath, fileStatus, fileProtocol}) + let fileSource = CF.plain <$> filePath + pure (ft, CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol}) messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m () messageUpdate ct@Contact {contactId, localDisplayName = c} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do @@ -3817,7 +3840,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do inline <- receiveInlineMode fInv Nothing fileChunkSize RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP - ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} + ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ msgMeta (CIRcvMsgContent $ MCFile "") ciFile Nothing False toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) whenContactNtfs user ct $ do @@ -3831,7 +3854,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do inline <- receiveInlineMode fInv Nothing fileChunkSize RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvGroupFileTransfer db userId m fInv inline fileChunkSize let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP - ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} + ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ msgMeta (CIRcvMsgContent $ MCFile "") ciFile Nothing False groupMsgToView gInfo m ci msgMeta let g = groupName' gInfo @@ -4737,10 +4760,9 @@ deleteGroupCI user gInfo ci@(CChatItem msgDir deletedItem@ChatItem {file}) byUse pure $ CRChatItemDeleted user (AChatItem SCTGroup msgDir (GroupChat gInfo) deletedItem) toCi byUser timed deleteCIFile :: (ChatMonad m, MsgDirectionI d) => User -> Maybe (CIFile d) -> m () -deleteCIFile user file = - forM_ file $ \CIFile {fileId, filePath, fileStatus} -> do - let fileInfo = CIFileInfo {fileId, fileStatus = Just $ AFS msgDirection fileStatus, filePath} - fileAgentConnIds <- deleteFile' user fileInfo True +deleteCIFile user file_ = + forM_ file_ $ \file -> do + fileAgentConnIds <- deleteFile' user (mkCIFileInfo file) True deleteAgentConnectionsAsync user fileAgentConnIds markDirectCIDeleted :: ChatMonad m => User -> Contact -> CChatItem 'CTDirect -> MessageId -> Bool -> UTCTime -> m ChatResponse @@ -4764,10 +4786,9 @@ markGroupCIDeleted user gInfo@GroupInfo {groupId} ci@(CChatItem _ ChatItem {file gItem (CChatItem msgDir ci') = AChatItem SCTGroup msgDir (GroupChat gInfo) ci' cancelCIFile :: (ChatMonad m, MsgDirectionI d) => User -> Maybe (CIFile d) -> m () -cancelCIFile user file = - forM_ file $ \CIFile {fileId, filePath, fileStatus} -> do - let fileInfo = CIFileInfo {fileId, fileStatus = Just $ AFS msgDirection fileStatus, filePath} - fileAgentConnIds <- cancelFile' user fileInfo True +cancelCIFile user file_ = + forM_ file_ $ \file -> do + fileAgentConnIds <- cancelFile' user (mkCIFileInfo file) True deleteAgentConnectionsAsync user fileAgentConnIds createAgentConnectionAsync :: forall m c. (ChatMonad m, ConnectionModeI c) => User -> CommandFunction -> Bool -> SConnectionMode c -> m (CommandId, ConnId) @@ -5000,7 +5021,7 @@ withAgent :: ChatMonad m => (AgentClient -> ExceptT AgentErrorType m a) -> m a withAgent action = asks smpAgent >>= runExceptT . action - >>= liftEither . first (\e -> ChatErrorAgent e Nothing) + >>= liftEither . first (`ChatErrorAgent` Nothing) withStore' :: ChatMonad m => (DB.Connection -> IO a) -> m a withStore' action = withStore $ liftIO . action @@ -5235,8 +5256,8 @@ chatCommandP = ("/fforward " <|> "/ff ") *> (ForwardFile <$> chatNameP' <* A.space <*> A.decimal), ("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal), ("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath), - ("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)), - "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal), + ("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> (" encrypt=" *> onOffP <|> pure False) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)), + "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> (" encrypt=" *> onOffP <|> pure False)), ("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal), ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), "/simplex" *> (ConnectSimplex <$> incognitoP), diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index 234963b44c..df9c66ceee 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -66,7 +66,7 @@ sendComposedMessage cc = sendComposedMessage' cc . contactId' sendComposedMessage' :: ChatController -> ContactId -> Maybe ChatItemId -> MsgContent -> IO () sendComposedMessage' cc ctId quotedItemId msgContent = do - let cm = ComposedMessage {filePath = Nothing, quotedItemId, msgContent} + let cm = ComposedMessage {fileSource = Nothing, quotedItemId, msgContent} sendChatCmd cc (APISendMessage (ChatRef CTDirect ctId) False Nothing cm) >>= \case CRNewChatItem {} -> printLog cc CLLInfo $ "sent message to contact ID " <> show ctId r -> putStrLn $ "unexpected send message response: " <> show r diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 615e472f25..f766edf0c5 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -22,8 +22,9 @@ import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader import Crypto.Random (ChaChaDRG) -import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.Aeson (FromJSON (..), ToJSON (..), (.:), (.:?)) import qualified Data.Aeson as J +import qualified Data.Aeson.Types as JT import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B @@ -54,16 +55,18 @@ import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration) +import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.File (CryptoFile (..)) +import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType, CorrId, MsgFlags, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SProtocolType, UserProtocol, XFTPServerWithAuth) import Simplex.Messaging.TMap (TMap) -import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import Simplex.Messaging.Transport (simplexMQVersion) import Simplex.Messaging.Transport.Client (TransportHost) -import Simplex.Messaging.Util (allFinally, catchAllErrors, tryAllErrors) +import Simplex.Messaging.Util (allFinally, catchAllErrors, tryAllErrors, (<$$>)) import System.IO (Handle) import System.Mem.Weak (Weak) import UnliftIO.STM @@ -387,8 +390,8 @@ data ChatCommand | ForwardFile ChatName FileTransferId | ForwardImage ChatName FileTransferId | SendFileDescription ChatName FilePath - | ReceiveFile {fileId :: FileTransferId, fileInline :: Maybe Bool, filePath :: Maybe FilePath} - | SetFileToReceive FileTransferId + | ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath} + | SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Bool} | CancelFile FileTransferId | FileStatus FileTransferId | ShowProfile -- UserId (not used in UI) @@ -723,11 +726,24 @@ data UserProfileUpdateSummary = UserProfileUpdateSummary instance ToJSON UserProfileUpdateSummary where toEncoding = J.genericToEncoding J.defaultOptions data ComposedMessage = ComposedMessage - { filePath :: Maybe FilePath, + { fileSource :: Maybe CryptoFile, quotedItemId :: Maybe ChatItemId, msgContent :: MsgContent } - deriving (Show, Generic, FromJSON) + deriving (Show, Generic) + +-- This instance is needed for backward compatibility, can be removed in v6.0 +instance FromJSON ComposedMessage where + parseJSON (J.Object v) = do + fileSource <- + (v .:? "fileSource") >>= \case + Nothing -> CF.plain <$$> (v .:? "filePath") + f -> pure f + quotedItemId <- v .:? "quotedItemId" + msgContent <- v .: "msgContent" + pure ComposedMessage {fileSource, quotedItemId, msgContent} + parseJSON invalid = + JT.prependFailure "bad ComposedMessage, " (JT.typeMismatch "Object" invalid) instance ToJSON ComposedMessage where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 33b6041841..45e5f9ff74 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -37,6 +37,8 @@ import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgMeta (..), MsgReceiptStatus (..)) +import Simplex.Messaging.Crypto.File (CryptoFile (..)) +import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, parseAll, sumTypeJSON) import Simplex.Messaging.Protocol (MsgBody) @@ -459,7 +461,7 @@ data CIFile (d :: MsgDirection) = CIFile { fileId :: Int64, fileName :: String, fileSize :: Integer, - filePath :: Maybe FilePath, -- local file path + fileSource :: Maybe CryptoFile, -- local file path with optional key and nonce fileStatus :: CIFileStatus d, fileProtocol :: FileProtocol } @@ -631,6 +633,14 @@ data CIFileInfo = CIFileInfo } deriving (Show) +mkCIFileInfo :: MsgDirectionI d => CIFile d -> CIFileInfo +mkCIFileInfo CIFile {fileId, fileStatus, fileSource} = + CIFileInfo + { fileId, + fileStatus = Just $ AFS msgDirection fileStatus, + filePath = CF.filePath <$> fileSource + } + data CIStatus (d :: MsgDirection) where CISSndNew :: CIStatus 'MDSnd CISSndSent :: SndCIStatusProgress -> CIStatus 'MDSnd diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs index 725cf74cf9..95c490a901 100644 --- a/src/Simplex/Chat/Messages/CIContent.hs +++ b/src/Simplex/Chat/Messages/CIContent.hs @@ -50,7 +50,7 @@ instance FromField AMsgDirection where fromField = fromIntField_ $ fmap fromMsgD instance ToField MsgDirection where toField = toField . msgDirectionInt -fromIntField_ :: (Typeable a) => (Int64 -> Maybe a) -> Field -> Ok a +fromIntField_ :: Typeable a => (Int64 -> Maybe a) -> Field -> Ok a fromIntField_ fromInt = \case f@(Field (SQLInteger i) _) -> case fromInt i of diff --git a/src/Simplex/Chat/Migrations/M20230827_file_encryption.hs b/src/Simplex/Chat/Migrations/M20230827_file_encryption.hs new file mode 100644 index 0000000000..2e659cac84 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20230827_file_encryption.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20230827_file_encryption where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20230827_file_encryption :: Query +m20230827_file_encryption = + [sql| +ALTER TABLE files ADD COLUMN file_crypto_key BLOB; +ALTER TABLE files ADD COLUMN file_crypto_nonce BLOB; +|] + +down_m20230827_file_encryption :: Query +down_m20230827_file_encryption = + [sql| +ALTER TABLE files DROP COLUMN file_crypto_key; +ALTER TABLE files DROP COLUMN file_crypto_nonce; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 76b7ba4a12..1badae2e1b 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -204,7 +204,9 @@ CREATE TABLE files( agent_snd_file_id BLOB NULL, private_snd_file_descr TEXT NULL, agent_snd_file_deleted INTEGER DEFAULT 0 CHECK(agent_snd_file_deleted NOT NULL), - protocol TEXT NOT NULL DEFAULT 'smp' + protocol TEXT NOT NULL DEFAULT 'smp', + file_crypto_key BLOB, + file_crypto_nonce BLOB ); CREATE TABLE snd_files( file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE, diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 6e62fbce06..0f4b262b70 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -35,6 +35,8 @@ import GHC.Generics (Generic) import Simplex.Chat import Simplex.Chat.Controller import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList) +import Simplex.Chat.Mobile.File +import Simplex.Chat.Mobile.Shared import Simplex.Chat.Mobile.WebRTC import Simplex.Chat.Options import Simplex.Chat.Store @@ -69,6 +71,10 @@ foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: CString -> Ptr Wo foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString +foreign export ccall "chat_write_file" cChatWriteFile :: CString -> Ptr Word8 -> CInt -> IO CJSONString + +foreign export ccall "chat_read_file" cChatReadFile :: CString -> CString -> CString -> IO (Ptr Word8) + -- | check / migrate database and initialize chat controller on success cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString cChatMigrateInit fp key conf ctrl = do @@ -151,8 +157,6 @@ defaultMobileConfig = logLevel = CLLError } -type CJSONString = CString - getActiveUser_ :: SQLiteStore -> IO (Maybe User) getActiveUser_ st = find activeUser <$> withTransaction st getUsers diff --git a/src/Simplex/Chat/Mobile/File.hs b/src/Simplex/Chat/Mobile/File.hs new file mode 100644 index 0000000000..4f73e191ad --- /dev/null +++ b/src/Simplex/Chat/Mobile/File.hs @@ -0,0 +1,83 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TupleSections #-} + +module Simplex.Chat.Mobile.File + ( cChatWriteFile, + cChatReadFile, + WriteFileResult (..), + ReadFileResult (..), + chatWriteFile, + chatReadFile, + ) +where + +import Control.Monad.Except +import Data.Aeson (ToJSON) +import qualified Data.Aeson as J +import Data.ByteString (ByteString) +import qualified Data.ByteString as B +import qualified Data.ByteString.Lazy as LB +import qualified Data.ByteString.Lazy.Char8 as LB' +import Data.Int (Int64) +import Data.Word (Word8) +import Foreign.C +import Foreign.Marshal.Alloc (mallocBytes) +import Foreign.Ptr +import GHC.Generics (Generic) +import Simplex.Chat.Mobile.Shared +import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) +import qualified Simplex.Messaging.Crypto.File as CF +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) + +data WriteFileResult + = WFResult {cryptoArgs :: CryptoFileArgs} + | WFError {writeError :: String} + deriving (Generic) + +instance ToJSON WriteFileResult where toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "WF" + +cChatWriteFile :: CString -> Ptr Word8 -> CInt -> IO CJSONString +cChatWriteFile cPath ptr len = do + path <- peekCAString cPath + s <- getByteString ptr len + r <- chatWriteFile path s + newCAString $ LB'.unpack $ J.encode r + +chatWriteFile :: FilePath -> ByteString -> IO WriteFileResult +chatWriteFile path s = do + cfArgs <- CF.randomArgs + let file = CryptoFile path $ Just cfArgs + either (WFError . show) (\_ -> WFResult cfArgs) + <$> runExceptT (CF.writeFile file $ LB.fromStrict s) + +data ReadFileResult + = RFResult {fileSize :: Int64} + | RFError {readError :: String} + deriving (Generic) + +instance ToJSON ReadFileResult where toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RF" + +cChatReadFile :: CString -> CString -> CString -> IO (Ptr Word8) +cChatReadFile cPath cKey cNonce = do + path <- peekCAString cPath + key <- B.packCString cKey + nonce <- B.packCString cNonce + (r, s) <- chatReadFile path key nonce + let r' = LB.toStrict (J.encode r) <> "\NUL" + ptr <- mallocBytes $ B.length r' + B.length s + putByteString ptr r' + unless (B.null s) $ putByteString (ptr `plusPtr` B.length r') s + pure ptr + +chatReadFile :: FilePath -> ByteString -> ByteString -> IO (ReadFileResult, ByteString) +chatReadFile path keyStr nonceStr = do + either ((,"") . RFError) (\s -> (RFResult $ LB.length s, LB.toStrict s)) <$> runExceptT readFile_ + where + readFile_ :: ExceptT String IO LB.ByteString + readFile_ = do + key <- liftEither $ strDecode keyStr + nonce <- liftEither $ strDecode nonceStr + let file = CryptoFile path $ Just $ CFArgs key nonce + withExceptT show $ CF.readFile file diff --git a/src/Simplex/Chat/Mobile/Shared.hs b/src/Simplex/Chat/Mobile/Shared.hs new file mode 100644 index 0000000000..a73a25fb6e --- /dev/null +++ b/src/Simplex/Chat/Mobile/Shared.hs @@ -0,0 +1,19 @@ +module Simplex.Chat.Mobile.Shared where + +import qualified Data.ByteString as B +import Data.ByteString.Internal (ByteString (PS), memcpy) +import Foreign.C (CInt, CString) +import Foreign (Ptr, Word8, newForeignPtr_, plusPtr) +import Foreign.ForeignPtr.Unsafe + +type CJSONString = CString + +getByteString :: Ptr Word8 -> CInt -> IO ByteString +getByteString ptr len = do + fp <- newForeignPtr_ ptr + pure $ PS fp 0 $ fromIntegral len + +putByteString :: Ptr Word8 -> ByteString -> IO () +putByteString ptr bs@(PS fp offset _) = do + let p = unsafeForeignPtrToPtr fp `plusPtr` offset + memcpy ptr p $ B.length bs diff --git a/src/Simplex/Chat/Mobile/WebRTC.hs b/src/Simplex/Chat/Mobile/WebRTC.hs index e05c9d609e..3fd5f018e2 100644 --- a/src/Simplex/Chat/Mobile/WebRTC.hs +++ b/src/Simplex/Chat/Mobile/WebRTC.hs @@ -12,16 +12,15 @@ import Control.Monad.Except import qualified Crypto.Cipher.Types as AES import Data.Bifunctor (bimap) import qualified Data.ByteArray as BA +import Data.ByteString (ByteString) import qualified Data.ByteString as B import qualified Data.ByteString.Base64.URL as U -import Data.ByteString.Internal (ByteString (PS), memcpy) import Data.Either (fromLeft) import Data.Word (Word8) import Foreign.C (CInt, CString, newCAString) -import Foreign.ForeignPtr (newForeignPtr_) -import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr) -import Foreign.Ptr (Ptr, plusPtr) +import Foreign.Ptr (Ptr) import qualified Simplex.Messaging.Crypto as C +import Simplex.Chat.Mobile.Shared cChatEncryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString cChatEncryptMedia = cTransformMedia chatEncryptMedia @@ -32,16 +31,10 @@ cChatDecryptMedia = cTransformMedia chatDecryptMedia cTransformMedia :: (ByteString -> ByteString -> ExceptT String IO ByteString) -> CString -> Ptr Word8 -> CInt -> IO CString cTransformMedia f cKey cFrame cFrameLen = do key <- B.packCString cKey - frame <- getFrame + frame <- getByteString cFrame cFrameLen runExceptT (f key frame >>= liftIO . putFrame) >>= newCAString . fromLeft "" where - getFrame = do - fp <- newForeignPtr_ cFrame - pure $ PS fp 0 $ fromIntegral cFrameLen - putFrame bs@(PS fp offset _) = do - let len = B.length bs - p = unsafeForeignPtrToPtr fp `plusPtr` offset - when (len <= fromIntegral cFrameLen) $ memcpy cFrame p len + putFrame s = when (B.length s < fromIntegral cFrameLen) $ putByteString cFrame s {-# INLINE cTransformMedia #-} chatEncryptMedia :: ByteString -> ByteString -> ExceptT String IO ByteString diff --git a/src/Simplex/Chat/Store/Files.hs b/src/Simplex/Chat/Store/Files.hs index 249dfedc37..9980352927 100644 --- a/src/Simplex/Chat/Store/Files.hs +++ b/src/Simplex/Chat/Store/Files.hs @@ -56,6 +56,7 @@ module Simplex.Chat.Store.Files startRcvInlineFT, xftpAcceptRcvFT, setRcvFileToReceive, + setFileCryptoArgs, getRcvFilesToReceive, setRcvFTAgentDeleted, updateRcvFileStatus, @@ -84,18 +85,21 @@ import Data.Time.Clock (UTCTime (..), getCurrentTime, nominalDay) import Data.Type.Equality import Database.SQLite.Simple (Only (..), (:.) (..)) import Database.SQLite.Simple.QQ (sql) +import Simplex.Chat.Messages +import Simplex.Chat.Messages.CIContent +import Simplex.Chat.Protocol import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Messages import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Shared -import Simplex.Chat.Messages -import Simplex.Chat.Messages.CIContent -import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Util (week) import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) +import qualified Simplex.Messaging.Crypto.File as CF getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer] getLiveSndFileTransfers db User {userId} = do @@ -257,14 +261,14 @@ getSndFTViaMsgDelivery db User {userId} Connection {connId, agentConnId} agentMs (\n -> SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, groupMemberId, recipientDisplayName = n, connId, agentConnId}) <$> (contactName_ <|> memberName_) -createSndFileTransferXFTP :: DB.Connection -> User -> ContactOrGroup -> FilePath -> FileInvitation -> AgentSndFileId -> Integer -> IO FileTransferMeta -createSndFileTransferXFTP db User {userId} contactOrGroup filePath FileInvitation {fileName, fileSize} agentSndFileId chunkSize = do +createSndFileTransferXFTP :: DB.Connection -> User -> ContactOrGroup -> CryptoFile -> FileInvitation -> AgentSndFileId -> Integer -> IO FileTransferMeta +createSndFileTransferXFTP db User {userId} contactOrGroup (CryptoFile filePath cryptoArgs) FileInvitation {fileName, fileSize} agentSndFileId chunkSize = do currentTs <- getCurrentTime - let xftpSndFile = Just XFTPSndFile {agentSndFileId, privateSndFileDescr = Nothing, agentSndFileDeleted = False} + let xftpSndFile = Just XFTPSndFile {agentSndFileId, privateSndFileDescr = Nothing, agentSndFileDeleted = False, cryptoArgs} DB.execute db - "INSERT INTO files (contact_id, group_id, user_id, file_name, file_path, file_size, chunk_size, agent_snd_file_id, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)" - (contactAndGroupIds contactOrGroup :. (userId, fileName, filePath, fileSize, chunkSize, agentSndFileId, CIFSSndStored, FPXFTP, currentTs, currentTs)) + "INSERT INTO files (contact_id, group_id, user_id, file_name, file_path, file_crypto_key, file_crypto_nonce, file_size, chunk_size, agent_snd_file_id, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + (contactAndGroupIds contactOrGroup :. (userId, fileName, filePath, CF.fileKey <$> cryptoArgs, CF.fileNonce <$> cryptoArgs, fileSize, chunkSize, agentSndFileId, CIFSSndStored, FPXFTP, currentTs, currentTs)) fileId <- insertedRowId db pure FileTransferMeta {fileId, xftpSndFile, fileName, filePath, fileSize, fileInline = Nothing, chunkSize, cancelled = False} @@ -479,7 +483,8 @@ createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@File currentTs <- liftIO getCurrentTime rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd_ - xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_ + -- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it + xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False, cryptoArgs = Nothing}) <$> rfd_ fileProtocol = if isJust rfd_ then FPXFTP else FPSMP fileId <- liftIO $ do DB.execute @@ -499,7 +504,8 @@ createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localD currentTs <- liftIO getCurrentTime rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd_ - xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_ + -- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it + xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False, cryptoArgs = Nothing}) <$> rfd_ fileProtocol = if isJust rfd_ then FPXFTP else FPSMP fileId <- liftIO $ do DB.execute @@ -600,7 +606,7 @@ getRcvFileTransfer db User {userId} fileId = do [sql| SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name, f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name, - f.file_path, r.file_inline, r.rcv_file_inline, r.agent_rcv_file_id, r.agent_rcv_file_deleted, c.connection_id, c.agent_conn_id + f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline, r.agent_rcv_file_id, r.agent_rcv_file_deleted, c.connection_id, c.agent_conn_id FROM rcv_files r JOIN files f USING (file_id) LEFT JOIN connections c ON r.file_id = c.rcv_file_id @@ -614,9 +620,9 @@ getRcvFileTransfer db User {userId} fileId = do where rcvFileTransfer :: Maybe RcvFileDescr -> - (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool) :. (Maybe Int64, Maybe AgentConnId) -> + (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool) :. (Maybe Int64, Maybe AgentConnId) -> ExceptT StoreError IO RcvFileTransfer - rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted) :. (connId_, agentConnId_)) = + rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted) :. (connId_, agentConnId_)) = case contactName_ <|> memberName_ of Nothing -> throwError $ SERcvFileInvalid fileId Just name -> do @@ -629,7 +635,8 @@ getRcvFileTransfer db User {userId} fileId = do where ft senderDisplayName fileStatus = let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing} - xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted}) <$> rfd_ + cryptoArgs = CFArgs <$> fileKey <*> fileNonce + xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted, cryptoArgs}) <$> rfd_ in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId} rfi = maybe (throwError $ SERcvFileInvalid fileId) pure =<< rfi_ rfi_ = case (filePath_, connId_, agentConnId_) of @@ -683,13 +690,21 @@ acceptRcvFT_ db User {userId} fileId filePath rcvFileInline currentTs = do "UPDATE rcv_files SET rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?" (rcvFileInline, FSAccepted, currentTs, fileId) -setRcvFileToReceive :: DB.Connection -> FileTransferId -> IO () -setRcvFileToReceive db fileId = do +setRcvFileToReceive :: DB.Connection -> FileTransferId -> Maybe CryptoFileArgs -> IO () +setRcvFileToReceive db fileId cfArgs_ = do currentTs <- getCurrentTime + DB.execute db "UPDATE rcv_files SET to_receive = 1, updated_at = ? WHERE file_id = ?" (currentTs, fileId) + forM_ cfArgs_ $ \cfArgs -> setFileCryptoArgs_ db fileId cfArgs currentTs + +setFileCryptoArgs :: DB.Connection -> FileTransferId -> CryptoFileArgs -> IO () +setFileCryptoArgs db fileId cfArgs = setFileCryptoArgs_ db fileId cfArgs =<< getCurrentTime + +setFileCryptoArgs_ :: DB.Connection -> FileTransferId -> CryptoFileArgs -> UTCTime -> IO () +setFileCryptoArgs_ db fileId (CFArgs key nonce) currentTs = DB.execute db - "UPDATE rcv_files SET to_receive = 1, updated_at = ? WHERE file_id = ?" - (currentTs, fileId) + "UPDATE files SET file_crypto_key = ?, file_crypto_nonce = ?, updated_at = ? WHERE file_id = ?" + (key, nonce, currentTs, fileId) getRcvFilesToReceive :: DB.Connection -> User -> IO [RcvFileTransfer] getRcvFilesToReceive db user@User {userId} = do @@ -842,15 +857,16 @@ getFileTransferMeta db User {userId} fileId = DB.query db [sql| - SELECT file_name, file_size, chunk_size, file_path, file_inline, agent_snd_file_id, agent_snd_file_deleted, private_snd_file_descr, cancelled + SELECT file_name, file_size, chunk_size, file_path, file_crypto_key, file_crypto_nonce, file_inline, agent_snd_file_id, agent_snd_file_deleted, private_snd_file_descr, cancelled FROM files WHERE user_id = ? AND file_id = ? |] (userId, fileId) where - fileTransferMeta :: (String, Integer, Integer, FilePath, Maybe InlineFileMode, Maybe AgentSndFileId, Bool, Maybe Text, Maybe Bool) -> FileTransferMeta - fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileInline, aSndFileId_, agentSndFileDeleted, privateSndFileDescr, cancelled_) = - let xftpSndFile = (\fId -> XFTPSndFile {agentSndFileId = fId, privateSndFileDescr, agentSndFileDeleted}) <$> aSndFileId_ + fileTransferMeta :: (String, Integer, Integer, FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe AgentSndFileId, Bool, Maybe Text, Maybe Bool) -> FileTransferMeta + fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileKey, fileNonce, fileInline, aSndFileId_, agentSndFileDeleted, privateSndFileDescr, cancelled_) = + let cryptoArgs = CFArgs <$> fileKey <*> fileNonce + xftpSndFile = (\fId -> XFTPSndFile {agentSndFileId = fId, privateSndFileDescr, agentSndFileDeleted, cryptoArgs}) <$> aSndFileId_ in FileTransferMeta {fileId, xftpSndFile, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = fromMaybe False cancelled_} getContactFileInfo :: DB.Connection -> User -> Contact -> IO [CIFileInfo] diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 7bc2eaf4d4..fb4e84c214 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -13,7 +13,6 @@ module Simplex.Chat.Store.Messages ( getContactConnIds_, getDirectChatReactions_, - toDirectChatItem, -- * Message and chat item functions deleteContactCIs, @@ -122,6 +121,8 @@ import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, MsgMeta (..), UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Util (eitherToMaybe) import UnliftIO.STM @@ -483,7 +484,7 @@ getDirectChatPreviews_ db user@User {userId} = do -- ChatItem i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol, + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- DirectQuote ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent FROM contacts ct @@ -548,7 +549,7 @@ getGroupChatPreviews_ db User {userId, userContactId} = do -- ChatItem i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol, + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- Maybe GroupMember - sender m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, @@ -669,7 +670,7 @@ getDirectChatItemsLast db User {userId} contactId count search = ExceptT $ do -- ChatItem i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol, + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- DirectQuote ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent FROM chat_items i @@ -698,7 +699,7 @@ getDirectChatAfter_ db User {userId} ct@Contact {contactId} afterChatItemId coun -- ChatItem i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol, + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- DirectQuote ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent FROM chat_items i @@ -728,7 +729,7 @@ getDirectChatBefore_ db User {userId} ct@Contact {contactId} beforeChatItemId co -- ChatItem i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol, + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- DirectQuote ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent FROM chat_items i @@ -950,7 +951,7 @@ type ChatStatsRow = (Int, ChatItemId, Bool) toChatStats :: ChatStatsRow -> ChatStats toChatStats (unreadCount, minUnreadItemId, unreadChat) = ChatStats {unreadCount, minUnreadItemId, unreadChat} -type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe ACIFileStatus, Maybe FileProtocol) +type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe ACIFileStatus, Maybe FileProtocol) type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe Bool) @@ -971,7 +972,7 @@ toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir -- this function can be changed so it never fails, not only avoid failure on invalid json toDirectChatItem :: UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect) -toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileStatus_, fileProtocol_)) :. quoteRow) = +toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) = chatItem $ fromRight invalid $ dbParseACIContent itemContentText where invalid = ACIContent msgDir $ CIInvalidJSON itemContentText @@ -988,7 +989,10 @@ toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentT maybeCIFile :: CIFileStatus d -> Maybe (CIFile d) maybeCIFile fileStatus = case (fileId_, fileName_, fileSize_, fileProtocol_) of - (Just fileId, Just fileName, Just fileSize, Just fileProtocol) -> Just CIFile {fileId, fileName, fileSize, filePath, fileStatus, fileProtocol} + (Just fileId, Just fileName, Just fileSize, Just fileProtocol) -> + let cfArgs = CFArgs <$> fileKey <*> fileNonce + fileSource = (`CryptoFile` cfArgs) <$> filePath + in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol} _ -> Nothing cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTDirect d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTDirect cItem d chatDir ciStatus content file = @@ -1021,7 +1025,7 @@ toGroupQuote qr@(_, _, _, _, quotedSent) quotedMember_ = toQuote qr $ direction -- this function can be changed so it never fails, not only avoid failure on invalid json toGroupChatItem :: UTCTime -> Int64 -> ChatItemRow :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow -> Either StoreError (CChatItem 'CTGroup) -toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileStatus_, fileProtocol_)) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do +toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do chatItem $ fromRight invalid $ dbParseACIContent itemContentText where member_ = toMaybeGroupMember userContactId memberRow_ @@ -1041,7 +1045,10 @@ toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, maybeCIFile :: CIFileStatus d -> Maybe (CIFile d) maybeCIFile fileStatus = case (fileId_, fileName_, fileSize_, fileProtocol_) of - (Just fileId, Just fileName, Just fileSize, Just fileProtocol) -> Just CIFile {fileId, fileName, fileSize, filePath, fileStatus, fileProtocol} + (Just fileId, Just fileName, Just fileSize, Just fileProtocol) -> + let cfArgs = CFArgs <$> fileKey <*> fileNonce + fileSource = (`CryptoFile` cfArgs) <$> filePath + in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol} _ -> Nothing cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTGroup d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTGroup cItem d chatDir ciStatus content file = @@ -1141,7 +1148,7 @@ updateDirectChatItemStatus db user@User {userId} contactId itemId itemStatus = d correctDir :: CChatItem c -> Either StoreError (ChatItem c d) correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci -updateDirectChatItem :: forall d. (MsgDirectionI d) => DB.Connection -> User -> Int64 -> ChatItemId -> CIContent d -> Bool -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d) +updateDirectChatItem :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItemId -> CIContent d -> Bool -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d) updateDirectChatItem db user contactId itemId newContent live msgId_ = do ci <- liftEither . correctDir =<< getDirectChatItem db user contactId itemId liftIO $ updateDirectChatItem' db user contactId ci newContent live msgId_ @@ -1149,7 +1156,7 @@ updateDirectChatItem db user contactId itemId newContent live msgId_ = do correctDir :: CChatItem c -> Either StoreError (ChatItem c d) correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci -updateDirectChatItem' :: forall d. (MsgDirectionI d) => DB.Connection -> User -> Int64 -> ChatItem 'CTDirect d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTDirect d) +updateDirectChatItem' :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTDirect d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTDirect d) updateDirectChatItem' db User {userId} contactId ci newContent live msgId_ = do currentTs <- liftIO getCurrentTime let ci' = updatedChatItem ci newContent live currentTs @@ -1294,7 +1301,7 @@ getDirectChatItem db User {userId} contactId itemId = ExceptT $ do -- ChatItem i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol, + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- DirectQuote ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent FROM chat_items i @@ -1469,7 +1476,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do -- ChatItem i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol, + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- GroupMember m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index 6da0d1cdcd..e8ac86c1e4 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -76,6 +76,7 @@ import Simplex.Chat.Migrations.M20230621_chat_item_moderations import Simplex.Chat.Migrations.M20230705_delivery_receipts import Simplex.Chat.Migrations.M20230721_group_snd_item_statuses import Simplex.Chat.Migrations.M20230814_indexes +import Simplex.Chat.Migrations.M20230827_file_encryption import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -151,7 +152,8 @@ schemaMigrations = ("20230621_chat_item_moderations", m20230621_chat_item_moderations, Just down_m20230621_chat_item_moderations), ("20230705_delivery_receipts", m20230705_delivery_receipts, Just down_m20230705_delivery_receipts), ("20230721_group_snd_item_statuses", m20230721_group_snd_item_statuses, Just down_m20230721_group_snd_item_statuses), - ("20230814_indexes", m20230814_indexes, Just down_m20230814_indexes) + ("20230814_indexes", m20230814_indexes, Just down_m20230814_indexes), + ("20230827_file_encryption", m20230827_file_encryption, Just down_m20230827_file_encryption) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index ac71ce6122..d427db6c68 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -42,6 +42,7 @@ import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Util import Simplex.FileTransfer.Description (FileDigest) import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, SAEntity (..), UserId) +import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, sumTypeJSON, taggedObjectJSON) import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI) @@ -345,11 +346,12 @@ data ChatSettings = ChatSettings instance ToJSON ChatSettings where toEncoding = J.genericToEncoding J.defaultOptions defaultChatSettings :: ChatSettings -defaultChatSettings = ChatSettings - { enableNtfs = True, - sendRcpts = Nothing, - favorite = False - } +defaultChatSettings = + ChatSettings + { enableNtfs = True, + sendRcpts = Nothing, + favorite = False + } pattern DisableNtfs :: ChatSettings pattern DisableNtfs <- ChatSettings {enableNtfs = False} @@ -953,7 +955,8 @@ instance ToJSON RcvFileTransfer where toEncoding = J.genericToEncoding J.default data XFTPRcvFile = XFTPRcvFile { rcvFileDescription :: RcvFileDescr, agentRcvFileId :: Maybe AgentRcvFileId, - agentRcvFileDeleted :: Bool + agentRcvFileDeleted :: Bool, + cryptoArgs :: Maybe CryptoFileArgs } deriving (Eq, Show, Generic) @@ -1108,7 +1111,8 @@ instance ToJSON FileTransferMeta where toEncoding = J.genericToEncoding J.defaul data XFTPSndFile = XFTPSndFile { agentSndFileId :: AgentSndFileId, privateSndFileDescr :: Maybe Text, - agentSndFileDeleted :: Bool + agentSndFileDeleted :: Bool, + cryptoArgs :: Maybe CryptoFileArgs } deriving (Eq, Show, Generic) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 033b1c9f7f..172155747e 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -50,6 +50,7 @@ import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..)) import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON) @@ -160,7 +161,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView CRRcvFileDescrReady _ _ -> [] CRRcvFileDescrNotReady _ _ -> [] CRRcvFileProgressXFTP {} -> [] - CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci + CRRcvFileAccepted u ci -> ttyUser u $ savingFile' testView ci CRRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft CRSndFileCancelled u _ ftm fts -> ttyUser u $ viewSndFileCancelled ftm fts CRRcvFileCancelled u _ ft -> ttyUser u $ receivingFile_ "cancelled" ft @@ -251,7 +252,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView CRSQLResult rows -> map plain rows CRSlowSQLQueries {chatQueries, agentQueries} -> let viewQuery SlowSQLQuery {query, queryStats = SlowQueryStats {count, timeMax, timeAvg}} = - "count: " <> sShow count + ("count: " <> sShow count) <> (" :: max: " <> sShow timeMax <> " ms") <> (" :: avg: " <> sShow timeAvg <> " ms") <> (" :: " <> plain (T.unwords $ T.lines query)) @@ -274,7 +275,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView <> ("pending subscriptions: " : map sShow pendingSubscriptions) CRConnectionDisabled entity -> viewConnectionEntityDisabled entity CRAgentRcvQueueDeleted acId srv aqId err_ -> - [ "completed deleting rcv queue, agent connection id: " <> sShow acId + [ ("completed deleting rcv queue, agent connection id: " <> sShow acId) <> (", server: " <> sShow srv) <> (", agent queue id: " <> sShow aqId) <> maybe "" (\e -> ", error: " <> sShow e) err_ @@ -327,7 +328,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView Just CIQuote {chatDir = quoteDir, content} -> Just (msgDirectionInt $ quoteMsgDirection quoteDir, msgContentText content) fPath = case file of - Just CIFile {filePath = Just fp} -> Just fp + Just CIFile {fileSource = Just (CryptoFile fp _)} -> Just fp _ -> Nothing testViewItem :: CChatItem c -> Maybe GroupMember -> Text testViewItem (CChatItem _ ci@ChatItem {meta = CIMeta {itemText}}) membership_ = @@ -950,7 +951,8 @@ viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} = viewContactInfo :: Contact -> ConnectionStats -> Maybe Profile -> [StyledString] viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}} stats incognitoProfile = - ["contact ID: " <> sShow contactId] <> viewConnectionStats stats + ["contact ID: " <> sShow contactId] + <> viewConnectionStats stats <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) l]) contactLink <> maybe ["you've shared main profile with this contact"] @@ -1269,8 +1271,8 @@ viewSentBroadcast mc s f ts tz time = prependFirst (highlight' "/feed" <> " (" < | otherwise = "" viewSentFileInvitation :: StyledString -> CIFile d -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString] -viewSentFileInvitation to CIFile {fileId, filePath, fileStatus} ts tz = case filePath of - Just fPath -> sentWithTime_ ts tz $ ttySentFile fPath +viewSentFileInvitation to CIFile {fileId, fileSource, fileStatus} ts tz = case fileSource of + Just (CryptoFile fPath _) -> sentWithTime_ ts tz $ ttySentFile fPath _ -> const [] where ttySentFile fPath = ["/f " <> to <> ttyFilePath fPath] <> cancelSending @@ -1338,14 +1340,20 @@ humanReadableSize size mB = kB * 1024 gB = mB * 1024 -savingFile' :: AChatItem -> [StyledString] -savingFile' (AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, filePath = Just filePath}, chatDir = CIDirectRcv}) = - ["saving file " <> sShow fileId <> " from " <> ttyContact c <> " to " <> plain filePath] -savingFile' (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, filePath = Just filePath}, chatDir = CIGroupRcv GroupMember {localDisplayName = m}}) = - ["saving file " <> sShow fileId <> " from " <> ttyContact m <> " to " <> plain filePath] -savingFile' (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, filePath = Just filePath}}) = - ["saving file " <> sShow fileId <> " to " <> plain filePath] -savingFile' _ = ["saving file"] -- shouldn't happen +savingFile' :: Bool -> AChatItem -> [StyledString] +savingFile' testView (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileSource = Just (CryptoFile filePath cfArgs_)}, chatDir}) = + let from = case (chat, chatDir) of + (DirectChat Contact {localDisplayName = c}, CIDirectRcv) -> " from " <> ttyContact c + (_, CIGroupRcv GroupMember {localDisplayName = m}) -> " from " <> ttyContact m + _ -> "" + in ["saving file " <> sShow fileId <> from <> " to " <> plain filePath] <> cfArgsStr + where + cfArgsStr = case cfArgs_ of + Just cfArgs@(CFArgs key nonce) + | testView -> [plain $ LB.unpack $ J.encode cfArgs] + | otherwise -> [plain $ "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce] + _ -> [] +savingFile' _ _ = ["saving file"] -- shouldn't happen receivingFile_' :: StyledString -> AChatItem -> [StyledString] receivingFile_' status (AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectRcv}) = @@ -1397,7 +1405,7 @@ viewFileTransferStatus (FTRcv ft@RcvFileTransfer {fileId, fileInvitation = FileI RFSCancelled Nothing -> "cancelled" viewFileTransferStatusXFTP :: AChatItem -> [StyledString] -viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, fileName, fileSize, fileStatus, filePath}}) = +viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, fileName, fileSize, fileStatus, fileSource}}) = case fileStatus of CIFSSndStored -> ["sending " <> fstr <> " just started"] CIFSSndTransfer progress total -> ["sending " <> fstr <> " in progress " <> fileProgressXFTP progress total fileSize] @@ -1407,7 +1415,7 @@ viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId CIFSRcvInvitation -> ["receiving " <> fstr <> " not accepted yet, use " <> highlight ("/fr " <> show fileId) <> " to receive file"] CIFSRcvAccepted -> ["receiving " <> fstr <> " just started"] CIFSRcvTransfer progress total -> ["receiving " <> fstr <> " progress " <> fileProgressXFTP progress total fileSize] - CIFSRcvComplete -> ["receiving " <> fstr <> " complete" <> maybe "" (\fp -> ", path: " <> plain fp) filePath] + CIFSRcvComplete -> ["receiving " <> fstr <> " complete" <> maybe "" (\(CryptoFile fp _) -> ", path: " <> plain fp) fileSource] CIFSRcvCancelled -> ["receiving " <> fstr <> " cancelled"] CIFSRcvError -> ["receiving " <> fstr <> " error"] CIFSInvalid text -> [fstr <> " invalid status: " <> plain text] diff --git a/stack.yaml b/stack.yaml index d86d8fe576..b4a7fcf06f 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 4c0b8a31d20870a23e120e243359901d8240f922 + commit: 5dc3d739b206edc2b4706ba0eef64ad4492e68e6 - github: kazu-yamamoto/http2 commit: b5a1b7200cf5bc7044af34ba325284271f6dff25 # - ../direct-sqlcipher diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index e612f3d09e..a0622556af 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -249,7 +249,7 @@ getTermLine cc = Just s -> do -- remove condition to always echo virtual terminal when (printOutput cc) $ do - -- when True $ do + -- when True $ do name <- userName cc putStrLn $ name <> ": " <> s pure s diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs index 4343b547c9..a0b0779ff4 100644 --- a/tests/ChatTests/Files.hs +++ b/tests/ChatTests/Files.hs @@ -8,14 +8,19 @@ import ChatClient import ChatTests.Utils import Control.Concurrent (threadDelay) import Control.Concurrent.Async (concurrently_) +import qualified Data.Aeson as J import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy.Char8 as LB import Simplex.Chat (roundedFDCount) import Simplex.Chat.Controller (ChatConfig (..), InlineFilesConfig (..), XFTPFileConfig (..), defaultInlineFilesConfig) +import Simplex.Chat.Mobile.File import Simplex.Chat.Options (ChatOpts (..)) import Simplex.FileTransfer.Client.Main (xftpClientCLI) import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..)) +import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) +import Simplex.Messaging.Encoding.String import Simplex.Messaging.Util (unlessM) -import System.Directory (copyFile, doesFileExist) +import System.Directory (copyFile, createDirectoryIfMissing, doesFileExist, getFileSize) import System.Environment (withArgs) import System.IO.Silently (capture_) import Test.Hspec @@ -59,6 +64,7 @@ chatFileTests = do describe "file transfer over XFTP" $ do it "round file description count" $ const testXFTPRoundFDCount it "send and receive file" testXFTPFileTransfer + it "send and receive locally encrypted files" testXFTPFileTransferEncrypted it "send and receive file, accepting after upload" testXFTPAcceptAfterUpload it "send and receive file in group" testXFTPGroupFileTransfer it "delete uploaded file" testXFTPDeleteUploadedFile @@ -1013,6 +1019,35 @@ testXFTPFileTransfer = where cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} +testXFTPFileTransferEncrypted :: HasCallStack => FilePath -> IO () +testXFTPFileTransferEncrypted = + testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do + src <- B.readFile "./tests/fixtures/test.pdf" + srcLen <- getFileSize "./tests/fixtures/test.pdf" + let srcPath = "./tests/tmp/alice/test.pdf" + createDirectoryIfMissing True "./tests/tmp/alice/" + createDirectoryIfMissing True "./tests/tmp/bob/" + WFResult cfArgs <- chatWriteFile srcPath src + let fileJSON = LB.unpack $ J.encode $ CryptoFile srcPath $ Just cfArgs + withXFTPServer $ do + connectUsers alice bob + alice ##> ("/_send @2 json {\"msgContent\":{\"type\":\"file\", \"text\":\"\"}, \"fileSource\": " <> fileJSON <> "}") + alice <# "/f @bob ./tests/tmp/alice/test.pdf" + alice <## "use /fc 1 to cancel sending" + bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + bob <## "use /fr 1 [/ | ] to receive it" + bob ##> "/fr 1 encrypt=on ./tests/tmp/bob/" + bob <## "saving file 1 from alice to ./tests/tmp/bob/test.pdf" + Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob + alice <## "completed uploading file 1 (test.pdf) for bob" + bob <## "started receiving file 1 (test.pdf) from alice" + bob <## "completed receiving file 1 (test.pdf) from alice" + (RFResult destLen, dest) <- chatReadFile "./tests/tmp/bob/test.pdf" (strEncode key) (strEncode nonce) + fromIntegral destLen `shouldBe` srcLen + dest `shouldBe` src + where + cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} + testXFTPAcceptAfterUpload :: HasCallStack => FilePath -> IO () testXFTPAcceptAfterUpload = testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do @@ -1447,7 +1482,7 @@ startFileTransfer alice bob = startFileTransfer' alice bob "test.jpg" "136.5 KiB / 139737 bytes" startFileTransfer' :: HasCallStack => TestCC -> TestCC -> String -> String -> IO () -startFileTransfer' cc1 cc2 fileName fileSize = startFileTransferWithDest' cc1 cc2 fileName fileSize $ Just "./tests/tmp" +startFileTransfer' cc1 cc2 fName fSize = startFileTransferWithDest' cc1 cc2 fName fSize $ Just "./tests/tmp" checkPartialTransfer :: HasCallStack => String -> IO () checkPartialTransfer fileName = do