diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift index c3e4805bf3..16974147c8 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -70,14 +70,14 @@ struct CIImageView: View { } private func imageView(_ img: UIImage) -> some View { - let w = img.size.width <= img.size.height ? maxWidth * 0.75 : img.imageData == nil ? .infinity : maxWidth + let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth DispatchQueue.main.async { imgWidth = w } return ZStack(alignment: .topTrailing) { if img.imageData == nil { Image(uiImage: img) .resizable() .scaledToFit() - .frame(maxWidth: w) + .frame(width: w) } else { SwiftyGif(image: img) .frame(width: w, height: w * img.size.height / img.size.width) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift index ff208fe58a..a3918e17bc 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -243,13 +243,13 @@ struct CIVideoView: View { } private func imageView(_ img: UIImage) -> some View { - let w = img.size.width <= img.size.height ? maxWidth * 0.75 : .infinity + let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth DispatchQueue.main.async { videoWidth = w } return ZStack(alignment: .topTrailing) { Image(uiImage: img) .resizable() .scaledToFit() - .frame(maxWidth: w) + .frame(width: w) loadingIndicator() } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 33bd46e393..cd2aa55bc3 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -514,6 +514,7 @@ struct ChatView: View { chat: chat, chatItem: ci, maxWidth: maxWidth, + itemWidth: maxWidth, composeState: $composeState, selectedMember: $selectedMember, chatView: self @@ -526,6 +527,7 @@ struct ChatView: View { @ObservedObject var chat: Chat var chatItem: ChatItem var maxWidth: CGFloat + @State var itemWidth: CGFloat @Binding var composeState: ComposeState @Binding var selectedMember: GMember? var chatView: ChatView @@ -654,7 +656,7 @@ struct ChatView: View { playbackState: $playbackState, playbackTime: $playbackTime ) - .uiKitContextMenu(maxWidth: maxWidth, menu: uiMenu, allowMenu: $allowMenu) + .uiKitContextMenu(hasImageOrVideo: ci.content.msgContent?.isImageOrVideo == true, maxWidth: maxWidth, itemWidth: $itemWidth, menu: uiMenu, allowMenu: $allowMenu) .accessibilityLabel("") if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 { chatItemReactions(ci) diff --git a/apps/ios/Shared/Views/Helpers/ContextMenu.swift b/apps/ios/Shared/Views/Helpers/ContextMenu.swift index 3b82d6eb95..9504d919ef 100644 --- a/apps/ios/Shared/Views/Helpers/ContextMenu.swift +++ b/apps/ios/Shared/Views/Helpers/ContextMenu.swift @@ -11,11 +11,20 @@ import UIKit import SwiftUI extension View { - func uiKitContextMenu(maxWidth: CGFloat, menu: Binding, allowMenu: Binding) -> some View { + func uiKitContextMenu(hasImageOrVideo: Bool, maxWidth: CGFloat, itemWidth: Binding, menu: Binding, allowMenu: Binding) -> some View { Group { if allowMenu.wrappedValue { - InteractionView(content: self, maxWidth: maxWidth, menu: menu) - .fixedSize(horizontal: true, vertical: false) + if hasImageOrVideo { + InteractionView(content: + self.environmentObject(ChatModel.shared) + .overlay(DetermineWidthImageVideoItem()) + .onPreferenceChange(DetermineWidthImageVideoItem.Key.self) { itemWidth.wrappedValue = $0 == 0 ? maxWidth : $0 } + , maxWidth: maxWidth, itemWidth: itemWidth, menu: menu) + .frame(maxWidth: itemWidth.wrappedValue) + } else { + InteractionView(content: self.environmentObject(ChatModel.shared), maxWidth: maxWidth, itemWidth: itemWidth, menu: menu) + .fixedSize(horizontal: true, vertical: false) + } } else { self } @@ -31,13 +40,14 @@ private class HostingViewHolder: UIView { struct InteractionView: UIViewRepresentable { let content: Content var maxWidth: CGFloat + var itemWidth: Binding @Binding var menu: UIMenu func makeUIView(context: Context) -> UIView { let view = HostingViewHolder() - view.contentSize = CGSizeMake(maxWidth, .infinity) view.backgroundColor = .clear let hostView = UIHostingController(rootView: content) + view.contentSize = hostView.view.intrinsicContentSize hostView.view.translatesAutoresizingMaskIntoConstraints = false let constraints = [ hostView.view.topAnchor.constraint(equalTo: view.topAnchor), @@ -57,7 +67,11 @@ struct InteractionView: UIViewRepresentable { } func updateUIView(_ uiView: UIView, context: Context) { - (uiView as! HostingViewHolder).contentSize = uiView.subviews[0].sizeThatFits(CGSizeMake(maxWidth, .infinity)) + let was = (uiView as! HostingViewHolder).contentSize + (uiView as! HostingViewHolder).contentSize = uiView.subviews[0].sizeThatFits(CGSizeMake(itemWidth.wrappedValue, .infinity)) + if was != (uiView as! HostingViewHolder).contentSize { + uiView.invalidateIntrinsicContentSize() + } } func makeCoordinator() -> Coordinator { diff --git a/apps/ios/Shared/Views/Helpers/DetermineWidth.swift b/apps/ios/Shared/Views/Helpers/DetermineWidth.swift index d2a0aaab1d..b05ab17089 100644 --- a/apps/ios/Shared/Views/Helpers/DetermineWidth.swift +++ b/apps/ios/Shared/Views/Helpers/DetermineWidth.swift @@ -21,6 +21,19 @@ struct DetermineWidth: View { } } +struct DetermineWidthImageVideoItem: View { + typealias Key = MaximumWidthImageVideoPreferenceKey + var body: some View { + GeometryReader { proxy in + Color.clear + .preference( + key: MaximumWidthImageVideoPreferenceKey.self, + value: proxy.size.width + ) + } + } +} + struct MaximumWidthPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { @@ -28,6 +41,13 @@ struct MaximumWidthPreferenceKey: PreferenceKey { } } +struct MaximumWidthImageVideoPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = max(value, nextValue()) + } +} + struct DetermineWidth_Previews: PreviewProvider { static var previews: some View { DetermineWidth() diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 5e1c0ac538..ed62b5c9ac 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -3209,6 +3209,14 @@ public enum MsgContent: Equatable { } } + public var isImageOrVideo: Bool { + switch self { + case .image: true + case .video: true + default: false + } + } + var cmdString: String { "json \(encodeJSON(self))" } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex.svg new file mode 100644 index 0000000000..d7019f3645 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index 44073aa990..7b7762eefc 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -21,6 +21,7 @@ import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.common.views.TerminalView import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import java.awt.event.WindowEvent @@ -103,7 +104,7 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState) { simplexWindowState.windowState = windowState // Reload all strings in all @Composable's after language change at runtime if (remember { ChatController.appPrefs.appLanguage.state }.value != "") { - Window(state = windowState, onCloseRequest = { closedByError.value = false; exitApplication() }, onKeyEvent = { + Window(state = windowState, icon = painterResource(MR.images.ic_simplex), onCloseRequest = { closedByError.value = false; exitApplication() }, onKeyEvent = { if (it.key == Key.Escape && it.type == KeyEventType.KeyUp) { simplexWindowState.backstack.lastOrNull()?.invoke() != null } else { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt index 8dac39199f..2c063b5888 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt @@ -7,6 +7,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import chat.simplex.common.platform.* +import chat.simplex.common.simplexWindowState +import java.awt.Window @Composable actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) { @@ -23,14 +25,15 @@ actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLon } } +/* +* This function doesn't take into account multi-window environment. In case more windows will be used, modify the code +* */ @Composable -actual fun LocalWindowWidth(): Dp { - return with(LocalDensity.current) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() } - /*val density = LocalDensity.current - var width by remember { mutableStateOf(with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() }) } - SideEffect { - if (width != with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() }) - width = with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() } +actual fun LocalWindowWidth(): Dp = with(LocalDensity.current) { + val windows = java.awt.Window.getWindows() + if (windows.size == 1) { + (windows.getOrNull(0)?.width ?: 0).toDp() + } else { + simplexWindowState.windowState.size.width } - return width.also { println("LALAL $it") }*/ } diff --git a/cabal.project b/cabal.project index 12879f4c76..33ac15fb7e 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: bfd532e833aff36754ef766f4e021f0079a7f83c + tag: 1e6268cc1dbba69639425f7d5a6c9a07995e0bb2 source-repository-package type: git @@ -34,12 +34,6 @@ source-repository-package location: https://github.com/simplex-chat/aeson.git tag: aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b --- old bs/text compat for 8.10 -source-repository-package - type: git - location: https://github.com/simplex-chat/base64.git - tag: 2d77b6dbcaffc00570a70be8694049f3710e7c94 - source-repository-package type: git location: https://github.com/simplex-chat/haskell-terminal.git diff --git a/docs/rfcs/2024-03-14-super-peers.md b/docs/rfcs/2024-03-14-super-peers.md new file mode 100644 index 0000000000..9996c02490 --- /dev/null +++ b/docs/rfcs/2024-03-14-super-peers.md @@ -0,0 +1,72 @@ +# Large public grups / channels + +## Background + +SimpleX Chat users participate in public groups that were created for small, fully connected p2p groups - working groups, teams, etc. The ability to join the groups via the links was added as an afterthought, without forward looking design, simply to accomodate the interest from the users to use SimpleX platform for public groups and communities. Overall, it's correct to say that the emergence of public groups was unexpected in the context of private messaging, and it shows that protecting participants and publishers identity, and having per-group identity is important for many people. + +## Problems of the current p2p design + +### It doesn't scale to large size + +Current design assumes that each peer is connected to each peer and sends messages to all. It creates non-trivial cost of establishing the connections as the group grows, some abandoned connections when some members remain in "connecting" state and also linearly growing traffic to send each message. + +Historically, there were p2p designs when peers connected not to all but some members, but they were mostly used by desktop clients with more persistent network connections, did not provide any asynchronous delivery and were trading lower traffic for latency and availability. Such designs were viable for file sharing across desktop devices, but it probably would not work well for dynamic real-time communities with active participation from a small share of members and the design of other members to observe the conversation as it happens. + +### It doesn't account for participation asymmetry + +Most members of large public groups do not send messages, so connecting them to all other members directly appears unnecessary and costly, and it also requires tracking when members became inactive to stop sending messages to them. + +## Objectives for the new design + +### Transcript integrity + +This issue is covered in detail in [Group integrity](./2023-10-20-group-integrity.md). + +### Asynchronous delivery to group + +Asynchronous delivery is important to protect participants privacy from traffic observation. In addition to that, sending scheduled posts is quite often a convenient feature to schedule multiple updates for a longer period of time - for example, schedule daily updates for a week, doing it once a week. + +### Ability to conceal members list + +This is a rather common request for the current groups, and while it could be possible of course to hide it on the client level, this still makes data available via the database, and puts less technical users in unfair position, while not protecting users privacy from technically competent members. + +### Support pre-moderation + +As the group size grows, so does the activity of the bad actors. Some groups will benefit from switching all, some, most, or new members to pre-moderation - when each post needs to be approved by admin before it becomes visible to all members. It would slow down the conversations, but it would allow a better content quality and owners' control of the content. + +## Channels based on super-peers + +The proposal is to model the new UX design from Telegram channels, with optional subchannels, and granular participation rights for the members / followers. + +Another consideration is to create democratically governed communities when creators don't own the community but only appoint the initial administrators, but as the community grows it can elect the new admins or moderators from the existing members, where voting power is somehow determined by the community score (which is necessary to compensate for anonymous participants who could subvert the vote if plain vote count was made). This is probably out of scope for the initial implementation, but this idea is very appealing and it doesn't exist in any other decentralised platforms. + +Technologically, the channel or group would determine which super-peers would host the group or channel, with group content being a merkle tree with ability to remove some content creating holes - which seems to be very important quality, both to remove undesirable content and to protect participants privacy. + +Super-peer would manage this merkle-tree state based on the messages from owners, admins and members, with the ability to make some destructive actions confirmed by more than one command. E.g., group/channel deletion may require at least 2 or 3 votes (respectively, for 3 and 5 owners), thus protecting both from accidental deletions and from attacks via owner - one of the owners being compromised won't result in group deletion if 2 votes are required. As the group size grows, owners can also modify rules (which in itself can also require m of n votes). + +## Joining group + +The current model when the link to join the group is, effectively, an address of one of the admins, is not censorship-resistant, reliable or convenient - the admin can be offline, be removed, etc. So we want to somehow include addresses of multiple super-peers to join the group. Without identity-layer, the addresses are quite large already, and including multiple addresses in one link, while possible, would make the qr code very hard to scan. Practically, without creating identity layer, we can use up to 2-3 super-peer addresses for the group, and increase it later. 2 addresses is likely to be satisfactory, as one of them could be super-peer hosted by SimpleX Chat, and another - by group owners. + +The client then will be connecting to all super-peers in the address. Once connected, these super-peers could send the addresses of the additional super-peers, but this is probably unnecessary for the initial release. + +## MVP + +The challenge is to decide what should be in scope for the initial release, to make it a valuable upgrade and a viable starting point, without overloading it with the functions that can be added later. + +MVP scope: + +- Core functioning of the group - creation/deletion/choosing and changing super-peers. While a large scope, it appears essential. +- Message delivery via super-peers. +- Super-peer protocol extension. Most likely super-peer would receive ordinary chat messages, but some operations should be added and require additional protocol messages - adding/removing super-peers to groups. +- Protocol extensions for owner actions with approvals - we already had several accidental deletions or lost owner accounts. Possibly, it is out of MVP scope. +- Search and history navigation. Current decision to send 100 messages both creates unnecessary traffic spikes, and also doesn't provide access to older history and search functions. But, possibly, it should also be in follow up improvements, and only should be included as the initial protocol design. +- New format of the group address to include more than one super-peer. +- Granular permissions and management model. While the user interface can evolve, the protocol and the scenarios, and also rules models seems better to be added from the beginning. + +Follow-up / improvements: +- More scalable client - we already observe scalability issues with directory service, so replacing SQLite with Postgres, if the group participation starts growing seems very important. + +Out of scope: +- additional super-peers. +- smart-contacts. While very tempting to generalise permissions and management model via smart contracts, that would radically increase complexity and delivery time. diff --git a/package.yaml b/package.yaml index 1f891df76b..22fe922174 100644 --- a/package.yaml +++ b/package.yaml @@ -18,6 +18,7 @@ dependencies: - async == 2.2.* - attoparsec == 0.14.* - base >= 4.7 && < 5 + - base64-bytestring >= 1.0 && < 1.3 - composition == 1.0.* - constraints >= 0.12 && < 0.14 - containers == 0.6.* diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 9942f3f4a5..30e24e93bc 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,10 +1,9 @@ { - "https://github.com/simplex-chat/simplexmq.git"."bfd532e833aff36754ef766f4e021f0079a7f83c" = "1xxcdadllimk2hgzz6aggvywr14zm2h0l62c92yvnyvps9j49gdx"; + "https://github.com/simplex-chat/simplexmq.git"."1e6268cc1dbba69639425f7d5a6c9a07995e0bb2" = "14pvy7vivvigxj876kwikxcy7c2jc30azwydfgvnlj4xwcclil8q"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; "https://github.com/simplex-chat/aeson.git"."aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b" = "0jz7kda8gai893vyvj96fy962ncv8dcsx71fbddyy8zrvc88jfrr"; - "https://github.com/simplex-chat/base64.git"."2d77b6dbcaffc00570a70be8694049f3710e7c94" = "0zdskk67fzqrrx1i29s3shp7fh9c0krmq5h6hq03qx0n3xy2m44b"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/simplex-chat/android-support.git"."9aa09f148089d6752ce563b14c2df1895718d806" = "0pbf2pf13v2kjzi397nr13f1h3jv0imvsq8rpiyy2qyx5vd50pqn"; "https://github.com/simplex-chat/zip.git"."bd421c6b19cc4c465cd7af1f6f26169fb8ee1ebc" = "1csqfjhvc8wb5h4kxxndmb6iw7b4ib9ff2n81hrizsmnf45a6gg0"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 3ab530e2d9..17ea518309 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -190,6 +190,7 @@ library , async ==2.2.* , attoparsec ==0.14.* , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -251,6 +252,7 @@ executable simplex-bot , async ==2.2.* , attoparsec ==0.14.* , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -313,6 +315,7 @@ executable simplex-bot-advanced , async ==2.2.* , attoparsec ==0.14.* , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -378,6 +381,7 @@ executable simplex-broadcast-bot , async ==2.2.* , attoparsec ==0.14.* , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -441,6 +445,7 @@ executable simplex-chat , async ==2.2.* , attoparsec ==0.14.* , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -510,6 +515,7 @@ executable simplex-directory-service , async ==2.2.* , attoparsec ==0.14.* , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -604,6 +610,7 @@ test-suite simplex-chat-test , async ==2.2.* , attoparsec ==0.14.* , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 2367294489..7948875180 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -29,6 +29,7 @@ import qualified Data.Attoparsec.ByteString.Char8 as A import Data.Bifunctor (bimap, first, second) import Data.ByteArray (ScrubbedBytes) import qualified Data.ByteArray as BA +import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB @@ -103,9 +104,8 @@ import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKNoPQ, pattern IKPQOff, pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding -import Simplex.Messaging.Encoding.Base64 (base64P) -import qualified Simplex.Messaging.Encoding.Base64 as B64 import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (base64P) import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index c24de3b2e1..b29543cf74 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -24,6 +24,7 @@ import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ import qualified Data.Attoparsec.ByteString.Char8 as A +import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Lazy.Char8 as LB import Data.Char (isSpace) import Data.Int (Int64) @@ -47,7 +48,6 @@ 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 qualified Simplex.Messaging.Encoding.Base64 as B64 import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, parseAll, sumTypeJSON) import Simplex.Messaging.Protocol (MsgBody) diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 9bc31fa2c7..5883c6042c 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -17,6 +17,7 @@ import qualified Data.Aeson.TH as JQ import Data.Bifunctor (first) import Data.ByteArray (ScrubbedBytes) import qualified Data.ByteArray as BA +import qualified Data.ByteString.Base64.URL as U import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB @@ -49,7 +50,6 @@ import Simplex.Messaging.Agent.Env.SQLite (createAgentStore) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, closeSQLiteStore, reopenSQLiteStore) import Simplex.Messaging.Client (defaultNetworkConfig) import qualified Simplex.Messaging.Crypto as C -import qualified Simplex.Messaging.Encoding.Base64.URL as U import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), BasicAuth (..), CorrId (..), ProtoServerWithAuth (..), ProtocolServer (..)) diff --git a/src/Simplex/Chat/Mobile/WebRTC.hs b/src/Simplex/Chat/Mobile/WebRTC.hs index b69203b651..537388b18b 100644 --- a/src/Simplex/Chat/Mobile/WebRTC.hs +++ b/src/Simplex/Chat/Mobile/WebRTC.hs @@ -17,6 +17,7 @@ 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.Either (fromLeft) import Data.Word (Word8) import Foreign.C (CInt, CString, newCAString) @@ -25,7 +26,6 @@ import Foreign.StablePtr import Simplex.Chat.Controller (ChatController (..)) import Simplex.Chat.Mobile.Shared import qualified Simplex.Messaging.Crypto as C -import qualified Simplex.Messaging.Encoding.Base64.URL as U import UnliftIO (atomically) cChatEncryptMedia :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CString diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 416b88599c..819c1cd670 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -22,6 +22,7 @@ import Crypto.Random (getRandomBytes) import qualified Data.Aeson as J import qualified Data.Aeson.Types as JT import Data.ByteString (ByteString) +import qualified Data.ByteString.Base64.URL as B64U import Data.ByteString.Builder (Builder) import qualified Data.ByteString.Char8 as B import Data.Functor (($>)) @@ -55,7 +56,6 @@ import Simplex.Messaging.Agent import Simplex.Messaging.Agent.Protocol (AgentErrorType (RCP)) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF -import qualified Simplex.Messaging.Encoding.Base64.URL as B64U import Simplex.Messaging.Encoding.String (StrEncoding (..)) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport (TLS, closeConnection, tlsUniq) diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index dc75ad50e9..88540134fe 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -18,6 +18,7 @@ import Control.Monad.Except import Control.Monad.IO.Class import Crypto.Random (ChaChaDRG) import qualified Data.Aeson.TH as J +import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) import Data.Int (Int64) import Data.Maybe (fromMaybe, isJust, listToMaybe) @@ -38,7 +39,6 @@ import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..)) import qualified Simplex.Messaging.Crypto.Ratchet as CR -import qualified Simplex.Messaging.Encoding.Base64 as B64 import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (SubscriptionMode (..)) import Simplex.Messaging.Util (allFinally) diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 13812cbaaa..3b0748e7d0 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -14,6 +14,7 @@ import Control.Concurrent.STM import Control.Monad (unless, when) import Control.Monad.Except (runExceptT) import Data.ByteString (ByteString) +import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Char8 as B import Data.Char (isDigit) import Data.List (isPrefixOf, isSuffixOf) @@ -34,7 +35,6 @@ import Simplex.Messaging.Agent.Store.SQLite (maybeFirstRow, withTransaction) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff) -import qualified Simplex.Messaging.Encoding.Base64 as B64 import Simplex.Messaging.Encoding.String import Simplex.Messaging.Version import System.Directory (doesFileExist) diff --git a/tests/WebRTCTests.hs b/tests/WebRTCTests.hs index f2372acc2f..a473afef36 100644 --- a/tests/WebRTCTests.hs +++ b/tests/WebRTCTests.hs @@ -4,12 +4,12 @@ module WebRTCTests where import Control.Monad.Except import Crypto.Random (getRandomBytes) +import qualified Data.ByteString.Base64.URL as U import qualified Data.ByteString.Char8 as B import Foreign.StablePtr import Simplex.Chat.Mobile import Simplex.Chat.Mobile.WebRTC import qualified Simplex.Messaging.Crypto as C -import qualified Simplex.Messaging.Encoding.Base64.URL as U import System.FilePath (()) import Test.Hspec @@ -36,8 +36,8 @@ webRTCTests = describe "WebRTC crypto" $ do cc <- newStablePtr c let key = B.replicate 32 '#' frame <- (<> B.replicate reservedSize '\NUL') <$> getRandomBytes 100 - runExceptT (chatEncryptMedia cc key frame) `shouldReturn` Left "invalid key: invalid base64 encoding near offset: 0" - runExceptT (chatDecryptMedia key frame) `shouldReturn` Left "invalid key: invalid base64 encoding near offset: 0" + runExceptT (chatEncryptMedia cc key frame) `shouldReturn` Left "invalid key: invalid character at offset: 0" + runExceptT (chatDecryptMedia key frame) `shouldReturn` Left "invalid key: invalid character at offset: 0" it "should fail on invalid auth tag" $ \tmp -> do Right c <- chatMigrateInit (tmp "1") "" "yesUp" cc <- newStablePtr c