From c2a320640b224dad03e05f72a1ecb836cb6fefbc Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 15 Oct 2023 18:16:12 +0100 Subject: [PATCH] core: local encryption for auto-received inline files (e.g. small voice messages) (#3224) * core: local encryption for auto-received inline files * update view, test --- apps/ios/Shared/Model/SimpleXAPI.swift | 7 +++ .../ios/SimpleX NSE/NotificationService.swift | 7 +++ apps/ios/SimpleXChat/APITypes.swift | 23 +++++---- .../chat/simplex/common/model/SimpleXAPI.kt | 20 +++++++- src/Simplex/Chat.hs | 44 ++++++++++------- src/Simplex/Chat/Controller.hs | 6 ++- src/Simplex/Chat/View.hs | 49 ++++++++++--------- tests/ChatTests/Files.hs | 33 ++++++++++++- 8 files changed, 133 insertions(+), 56 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index a1c8cee77..91c5b1b36 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -257,6 +257,12 @@ func setXFTPConfig(_ cfg: XFTPFileConfig?) throws { throw r } +func apiSetEncryptLocalFiles(_ enable: Boolean) throws { + let r = chatSendCmdSync(.apiSetEncryptLocalFiles(enable: enable)) + if case .cmdOk = r { return } + throw r +} + func apiExportArchive(config: ArchiveConfig) async throws { try await sendCommandOkResp(.apiExportArchive(config: config)) } @@ -1152,6 +1158,7 @@ func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool try apiSetTempFolder(tempFolder: getTempFilesDirectory().path) try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) try setXFTPConfig(getXFTPCfg()) + // try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get()) m.chatInitialized = true m.currentUser = try apiGetActiveUser() if m.currentUser == nil { diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 645fdb595..2fb069a86 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -216,6 +216,7 @@ func startChat() -> DBMigrationResult? { try apiSetTempFolder(tempFolder: getTempFilesDirectory().path) try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) try setXFTPConfig(xftpConfig) + // try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get()) let justStarted = try apiStartChat() chatStarted = true if justStarted { @@ -351,6 +352,12 @@ func setXFTPConfig(_ cfg: XFTPFileConfig?) throws { throw r } +func apiSetEncryptLocalFiles(_ enable: Boolean) throws { + let r = chatSendCmdSync(.apiSetEncryptLocalFiles(enable: enable)) + if case .cmdOk = r { return } + throw r +} + func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { guard apiGetActiveUser() != nil else { logger.debug("no active user") diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 9eb9b9084..ddab4c73b 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -32,6 +32,7 @@ public enum ChatCommand { case setTempFolder(tempFolder: String) case setFilesFolder(filesFolder: String) case apiSetXFTPConfig(config: XFTPFileConfig?) + case apiSetEncryptLocalFiles(enable: Bool) case apiExportArchive(config: ArchiveConfig) case apiImportArchive(config: ArchiveConfig) case apiDeleteStorage @@ -114,8 +115,8 @@ public enum ChatCommand { case apiGetNetworkStatuses case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) - case receiveFile(fileId: Int64, encrypted: Bool, inline: Bool?) - case setFileToReceive(fileId: Int64, encrypted: Bool) + case receiveFile(fileId: Int64, encrypted: Bool?, inline: Bool?) + case setFileToReceive(fileId: Int64, encrypted: Bool?) case cancelFile(fileId: Int64) case showVersion case string(String) @@ -152,6 +153,7 @@ public enum ChatCommand { } else { return "/_xftp off" } + case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))" case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))" case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" case .apiDeleteStorage: return "/_db delete" @@ -247,13 +249,8 @@ public enum ChatCommand { case .apiGetNetworkStatuses: return "/_network_statuses" case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)" case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))" - case let .receiveFile(fileId, encrypted, inline): - let s = "/freceive \(fileId) encrypt=\(onOff(encrypted))" - if let inline = inline { - return s + " inline=\(onOff(inline))" - } - return s - case let .setFileToReceive(fileId, encrypted): return "/_set_file_to_receive \(fileId) encrypt=\(onOff(encrypted))" + case let .receiveFile(fileId, encrypt, inline): return "/freceive \(fileId)\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))" + case let .setFileToReceive(fileId, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("encrypt", encrypt))" case let .cancelFile(fileId): return "/fcancel \(fileId)" case .showVersion: return "/version" case let .string(str): return str @@ -283,6 +280,7 @@ public enum ChatCommand { case .setTempFolder: return "setTempFolder" case .setFilesFolder: return "setFilesFolder" case .apiSetXFTPConfig: return "apiSetXFTPConfig" + case .apiSetEncryptLocalFiles: return "apiSetEncryptLocalFiles" case .apiExportArchive: return "apiExportArchive" case .apiImportArchive: return "apiImportArchive" case .apiDeleteStorage: return "apiDeleteStorage" @@ -420,6 +418,13 @@ public enum ChatCommand { b ? "on" : "off" } + private func onOffParam(_ param: String, _ b: Bool?) -> String { + if let b = b { + return " \(param)=\(onOff(b))" + } + return "" + } + private func maybePwd(_ pwd: String?) -> String { pwd == "" || pwd == nil ? "" : " " + encodeJSON(pwd) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index e19520093..f49984b94 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -338,6 +338,7 @@ object ChatController { apiSetTempFolder(coreTmpDir.absolutePath) apiSetFilesFolder(appFilesDir.absolutePath) apiSetXFTPConfig(getXFTPCfg()) +// apiSetEncryptLocalFiles(appPrefs.privacyEncryptLocalFiles.get()) val justStarted = apiStartChat() val users = listUsers() chatModel.users.clear() @@ -559,6 +560,8 @@ object ChatController { throw Error("apiSetXFTPConfig bad response: ${r.responseType} ${r.details}") } + suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(CC.ApiSetEncryptLocalFiles(enable)) + suspend fun apiExportArchive(config: ArchiveConfig) { val r = sendCmd(CC.ApiExportArchive(config)) if (r is CR.CmdOk) return @@ -1327,6 +1330,13 @@ object ChatController { } } + private suspend fun sendCommandOkResp(cmd: CC): Boolean { + val r = sendCmd(cmd) + val ok = r is CR.CmdOk + if (!ok) apiErrorAlert(cmd.cmdType, generalGetString(MR.strings.error), r) + return ok + } + suspend fun apiGetVersion(): CoreVersionInfo? { val r = sendCmd(CC.ShowVersion()) return if (r is CR.VersionInfo) { @@ -1858,6 +1868,7 @@ sealed class CC { class SetTempFolder(val tempFolder: String): CC() class SetFilesFolder(val filesFolder: String): CC() class ApiSetXFTPConfig(val config: XFTPFileConfig?): CC() + class ApiSetEncryptLocalFiles(val enable: Boolean): CC() class ApiExportArchive(val config: ArchiveConfig): CC() class ApiImportArchive(val config: ArchiveConfig): CC() class ApiDeleteStorage: CC() @@ -1931,7 +1942,7 @@ sealed class CC { class ApiRejectContact(val contactReqId: Long): CC() class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC() class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC() - class ReceiveFile(val fileId: Long, val encrypted: Boolean, val inline: Boolean?): CC() + class ReceiveFile(val fileId: Long, val encrypt: Boolean?, val inline: Boolean?): CC() class CancelFile(val fileId: Long): CC() class ShowVersion(): CC() @@ -1963,6 +1974,7 @@ sealed class CC { is SetTempFolder -> "/_temp_folder $tempFolder" is SetFilesFolder -> "/_files_folder $filesFolder" is ApiSetXFTPConfig -> if (config != null) "/_xftp on ${json.encodeToString(config)}" else "/_xftp off" + is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}" is ApiExportArchive -> "/_db export ${json.encodeToString(config)}" is ApiImportArchive -> "/_db import ${json.encodeToString(config)}" is ApiDeleteStorage -> "/_db delete" @@ -2039,7 +2051,10 @@ sealed class CC { is ApiGetNetworkStatuses -> "/_network_statuses" is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}" is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}" - is ReceiveFile -> "/freceive $fileId encrypt=${onOff(encrypted)}" + (if (inline == null) "" else " inline=${onOff(inline)}") + is ReceiveFile -> + "/freceive $fileId" + + (if (encrypt == null) "" else " encrypt=${onOff(encrypt)}") + + (if (inline == null) "" else " inline=${onOff(inline)}") is CancelFile -> "/fcancel $fileId" is ShowVersion -> "/version" } @@ -2063,6 +2078,7 @@ sealed class CC { is SetTempFolder -> "setTempFolder" is SetFilesFolder -> "setFilesFolder" is ApiSetXFTPConfig -> "apiSetXFTPConfig" + is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles" is ApiExportArchive -> "apiExportArchive" is ApiImportArchive -> "apiImportArchive" is ApiDeleteStorage -> "apiDeleteStorage" diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 56077c3bf..90a661705 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -209,6 +209,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen cleanupManagerAsync <- newTVarIO Nothing timedItemThreads <- atomically TM.empty showLiveItems <- newTVarIO False + encryptLocalFiles <- newTVarIO False userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg tempDirectory <- newTVarIO tempDir contactMergeEnabled <- newTVarIO True @@ -236,6 +237,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen cleanupManagerAsync, timedItemThreads, showLiveItems, + encryptLocalFiles, userXFTPFileConfig, tempDirectory, logFilePath = logFile, @@ -515,6 +517,7 @@ processChatCommand = \case APISetXFTPConfig cfg -> do asks userXFTPFileConfig >>= atomically . (`writeTVar` cfg) ok_ + APISetEncryptLocalFiles on -> chatWriteVar encryptLocalFiles on >> ok_ SetContactMergeEnabled onOff -> do asks contactMergeEnabled >>= atomically . (`writeTVar` onOff) ok_ @@ -1773,19 +1776,16 @@ processChatCommand = \case ForwardFile chatName fileId -> forwardFile chatName fileId SendFile ForwardImage chatName fileId -> forwardFile chatName fileId SendImage SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO" - ReceiveFile fileId encrypted rcvInline_ filePath_ -> withUser $ \_ -> + ReceiveFile fileId encrypted_ rcvInline_ filePath_ -> withUser $ \_ -> withChatLock "receiveFile" . procCmd $ do (user, ft) <- withStore (`getRcvFileTransferById` fileId) - ft' <- if encrypted then encryptLocalFile ft else pure ft + encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles + ft' <- (if encrypt then setFileToEncrypt else pure) ft receiveFile' user ft' rcvInline_ filePath_ - where - encryptLocalFile ft = do - cfArgs <- liftIO $ CF.randomArgs - withStore' $ \db -> setFileCryptoArgs db fileId cfArgs - pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs} - SetFileToReceive fileId encrypted -> withUser $ \_ -> do + SetFileToReceive fileId encrypted_ -> withUser $ \_ -> do withChatLock "setFileToReceive" . procCmd $ do - cfArgs <- if encrypted then Just <$> liftIO CF.randomArgs else pure Nothing + encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles + cfArgs <- if encrypt then Just <$> liftIO CF.randomArgs else pure Nothing withStore' $ \db -> setRcvFileToReceive db fileId cfArgs ok_ CancelFile fileId -> withUser $ \user@User {userId} -> @@ -2410,6 +2410,12 @@ toFSFilePath :: ChatMonad' m => FilePath -> m FilePath toFSFilePath f = maybe f ( f) <$> (readTVarIO =<< asks filesFolder) +setFileToEncrypt :: ChatMonad m => RcvFileTransfer -> m RcvFileTransfer +setFileToEncrypt ft@RcvFileTransfer {fileId} = do + cfArgs <- liftIO CF.randomArgs + withStore' $ \db -> setFileCryptoArgs db fileId cfArgs + pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs} + receiveFile' :: ChatMonad m => User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> m ChatResponse receiveFile' user ft rcvInline_ filePath_ = do (CRRcvFileAccepted user <$> acceptFileReceive user ft rcvInline_ filePath_) `catchChatError` processError @@ -3931,14 +3937,17 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do inline <- receiveInlineMode fInv (Just mc) fileChunkSize ft@RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFT db fInv inline fileChunkSize let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP - (filePath, fileStatus) <- case inline of + (filePath, fileStatus, ft') <- case inline of Just IFMSent -> do + encrypt <- chatReadVar encryptLocalFiles + ft' <- (if encrypt then setFileToEncrypt else pure) ft fPath <- getRcvFilePath fileId Nothing fileName True - withStore' $ \db -> startRcvInlineFT db user ft fPath inline - pure (Just fPath, CIFSRcvAccepted) - _ -> pure (Nothing, CIFSRcvInvitation) - let fileSource = CF.plain <$> filePath - pure (ft, CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol}) + withStore' $ \db -> startRcvInlineFT db user ft' fPath inline + pure (Just fPath, CIFSRcvAccepted, ft') + _ -> pure (Nothing, CIFSRcvInvitation, ft) + let RcvFileTransfer {cryptoArgs} = ft' + fileSource = (`CryptoFile` cryptoArgs) <$> 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} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do @@ -5567,6 +5576,7 @@ chatCommandP = ("/_files_folder " <|> "/files_folder ") *> (SetFilesFolder <$> filePath), "/_xftp " *> (APISetXFTPConfig <$> ("on " *> (Just <$> jsonP) <|> ("off" $> Nothing))), "/xftp " *> (APISetXFTPConfig <$> ("on" *> (Just <$> xftpCfgP) <|> ("off" $> Nothing))), + "/_files_encrypt " *> (APISetEncryptLocalFiles <$> onOffP), "/contact_merge " *> (SetContactMergeEnabled <$> onOffP), "/_db export " *> (APIExportArchive <$> jsonP), "/db export" $> ExportArchive, @@ -5743,8 +5753,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 <*> (" encrypt=" *> onOffP <|> pure False) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)), - "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> (" encrypt=" *> onOffP <|> pure False)), + ("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> optional (" encrypt=" *> onOffP) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)), + "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> optional (" encrypt=" *> onOffP)), ("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal), ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), "/simplex" *> (ConnectSimplex <$> incognitoP), diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 2704c2721..0a9b88694 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -179,6 +179,7 @@ data ChatController = ChatController cleanupManagerAsync :: TVar (Maybe (Async ())), timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))), showLiveItems :: TVar Bool, + encryptLocalFiles :: TVar Bool, userXFTPFileConfig :: TVar (Maybe XFTPFileConfig), tempDirectory :: TVar (Maybe FilePath), logFilePath :: Maybe FilePath, @@ -221,6 +222,7 @@ data ChatCommand | SetTempFolder FilePath | SetFilesFolder FilePath | APISetXFTPConfig (Maybe XFTPFileConfig) + | APISetEncryptLocalFiles Bool | SetContactMergeEnabled Bool | APIExportArchive ArchiveConfig | ExportArchive @@ -393,8 +395,8 @@ data ChatCommand | ForwardFile ChatName FileTransferId | ForwardImage ChatName FileTransferId | SendFileDescription ChatName FilePath - | ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath} - | SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Bool} + | ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Maybe Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath} + | SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Maybe Bool} | CancelFile FileTransferId | FileStatus FileTransferId | ShowProfile -- UserId (not used in UI) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index bcbc45f4e..08b1579bd 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -166,7 +166,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView CRRcvFileDescrReady _ _ -> [] CRRcvFileDescrNotReady _ _ -> [] CRRcvFileProgressXFTP {} -> [] - CRRcvFileAccepted u ci -> ttyUser u $ savingFile' testView ci + CRRcvFileAccepted u ci -> ttyUser u $ savingFile' 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 @@ -178,10 +178,10 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView CRContactUpdated {user = u, fromContact = c, toContact = c'} -> ttyUser u $ viewContactUpdated c c' <> viewContactPrefsUpdated u c c' CRContactsMerged u intoCt mergedCt ct' -> ttyUser u $ viewContactsMerged intoCt mergedCt ct' CRReceivedContactRequest u UserContactRequest {localDisplayName = c, profile} -> ttyUser u $ viewReceivedContactRequest c profile - CRRcvFileStart u ci -> ttyUser u $ receivingFile_' "started" ci - CRRcvFileComplete u ci -> ttyUser u $ receivingFile_' "completed" ci + CRRcvFileStart u ci -> ttyUser u $ receivingFile_' testView "started" ci + CRRcvFileComplete u ci -> ttyUser u $ receivingFile_' testView "completed" ci CRRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft - CRRcvFileError u ci e -> ttyUser u $ receivingFile_' "error" ci <> [sShow e] + CRRcvFileError u ci e -> ttyUser u $ receivingFile_' testView "error" ci <> [sShow e] CRSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft CRSndFileStartXFTP {} -> [] @@ -1449,27 +1449,28 @@ humanReadableSize size mB = kB * 1024 gB = mB * 1024 -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 +savingFile' :: AChatItem -> [StyledString] +savingFile' (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileSource = Just (CryptoFile filePath _)}, chatDir}) = + ["saving file " <> sShow fileId <> fileFrom chat chatDir <> " to " <> plain filePath] +savingFile' _ = ["saving file"] -- shouldn't happen -receivingFile_' :: StyledString -> AChatItem -> [StyledString] -receivingFile_' status (AChatItem _ _ (DirectChat c) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectRcv}) = - [status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyContact' c] -receivingFile_' status (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupRcv m}) = - [status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyMember m] -receivingFile_' status _ = [status <> " receiving file"] -- shouldn't happen +receivingFile_' :: Bool -> String -> AChatItem -> [StyledString] +receivingFile_' testView status (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileName, fileSource = Just (CryptoFile _ cfArgs_)}, chatDir}) = + [plain status <> " receiving " <> fileTransferStr fileId fileName <> fileFrom chat chatDir] <> cfArgsStr cfArgs_ + where + cfArgsStr (Just cfArgs@(CFArgs key nonce)) = [plain s | status == "completed"] + where + s = + if testView + then LB.toStrict $ J.encode cfArgs + else "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce + cfArgsStr _ = [] +receivingFile_' _ status _ = [plain status <> " receiving file"] -- shouldn't happen + +fileFrom :: ChatInfo c -> CIDirection c d -> StyledString +fileFrom (DirectChat ct) CIDirectRcv = " from " <> ttyContact' ct +fileFrom _ (CIGroupRcv m) = " from " <> ttyMember m +fileFrom _ _ = "" receivingFile_ :: StyledString -> RcvFileTransfer -> [StyledString] receivingFile_ status ft@RcvFileTransfer {senderDisplayName = c} = diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs index 50f86d8e0..523128390 100644 --- a/tests/ChatTests/Files.hs +++ b/tests/ChatTests/Files.hs @@ -33,6 +33,7 @@ chatFileTests = do describe "send and receive file" $ fileTestMatrix2 runTestFileTransfer describe "send file, receive and locally encrypt file" $ fileTestMatrix2 runTestFileTransferEncrypted it "send and receive file inline (without accepting)" testInlineFileTransfer + it "send inline file, receive (without accepting) and locally encrypt" testInlineFileTransferEncrypted xit'' "accept inline file transfer, sender cancels during transfer" testAcceptInlineFileSndCancelDuringTransfer it "send and receive small file inline (default config)" testSmallInlineFileTransfer it "small file sent without acceptance is ignored in terminal by default" testSmallInlineFileIgnored @@ -107,7 +108,6 @@ runTestFileTransferEncrypted alice bob = do bob <## "use /fr 1 [/ | ] to receive it" bob ##> "/fr 1 encrypt=on ./tests/tmp" bob <## "saving file 1 from alice to ./tests/tmp/test.pdf" - Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob concurrently_ (bob <## "started receiving file 1 (test.pdf) from alice") (alice <## "started sending file 1 (test.pdf) to bob") @@ -123,6 +123,7 @@ runTestFileTransferEncrypted alice bob = do "completed sending file 1 (test.pdf) to bob" ] ] + Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob src <- B.readFile "./tests/fixtures/test.pdf" -- dest <- B.readFile "./tests/tmp/test.pdf" -- dest `shouldBe` src @@ -154,6 +155,34 @@ testInlineFileTransfer = where cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, receiveChunks = 100}} +testInlineFileTransferEncrypted :: HasCallStack => FilePath -> IO () +testInlineFileTransferEncrypted = + testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do + connectUsers alice bob + bob ##> "/_files_folder ./tests/tmp/" + bob <## "ok" + bob ##> "/_files_encrypt on" + bob <## "ok" + alice ##> "/_send @2 json {\"msgContent\":{\"type\":\"voice\", \"duration\":10, \"text\":\"\"}, \"filePath\":\"./tests/fixtures/test.jpg\"}" + alice <# "@bob voice message (00:10)" + alice <# "/f @bob ./tests/fixtures/test.jpg" + -- below is not shown in "sent" mode + -- alice <## "use /fc 1 to cancel sending" + bob <# "alice> voice message (00:10)" + bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" + -- below is not shown in "sent" mode + -- bob <## "use /fr 1 [/ | ] to receive it" + bob <## "started receiving file 1 (test.jpg) from alice" + concurrently_ + (alice <## "completed sending file 1 (test.jpg) to bob") + (bob <## "completed receiving file 1 (test.jpg) from alice") + Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob + src <- B.readFile "./tests/fixtures/test.jpg" + Right dest <- chatReadFile "./tests/tmp/test.jpg" (strEncode key) (strEncode nonce) + LB.toStrict dest `shouldBe` src + where + cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, receiveChunks = 100}} + testAcceptInlineFileSndCancelDuringTransfer :: HasCallStack => FilePath -> IO () testAcceptInlineFileSndCancelDuringTransfer = testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do @@ -1077,10 +1106,10 @@ testXFTPFileTransferEncrypted = 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" + Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob Right dest <- chatReadFile "./tests/tmp/bob/test.pdf" (strEncode key) (strEncode nonce) LB.length dest `shouldBe` fromIntegral srcLen LB.toStrict dest `shouldBe` src