Files
simplex-chat/src/Simplex/Chat/Mobile/File.hs
T
Evgeny Poberezkin 7bcda7e54b core: use ChaChaDRG as the source of randomness (#3551)
* core: use ChaChaDRG as the source of randomness

* do not use entropy directly

* dont use RNG from agent

* simplexmq

* update iOS
2023-12-21 00:42:40 +00:00

146 lines
5.2 KiB
Haskell

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
module Simplex.Chat.Mobile.File
( cChatWriteFile,
cChatReadFile,
cChatEncryptFile,
cChatDecryptFile,
WriteFileResult (..),
ReadFileResult (..),
chatWriteFile,
chatReadFile,
)
where
import Control.Monad
import Control.Monad.Except
import Control.Monad.IO.Class
import qualified Data.Aeson as J
import qualified Data.Aeson.TH as JQ
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.Char (chr)
import Data.Either (fromLeft)
import Data.Word (Word32, Word8)
import Foreign.C
import Foreign.Marshal.Alloc (mallocBytes)
import Foreign.Ptr
import Foreign.StablePtr
import Foreign.Storable (poke, pokeByteOff)
import Simplex.Chat.Controller (ChatController (..))
import Simplex.Chat.Mobile.Shared
import Simplex.Chat.Util (chunkSize, encryptFile)
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..), CryptoFileHandle, FTCryptoError (..))
import qualified Simplex.Messaging.Crypto.File as CF
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
import Simplex.Messaging.Util (catchAll)
import UnliftIO (Handle, IOMode (..), atomically, withFile)
data WriteFileResult
= WFResult {cryptoArgs :: CryptoFileArgs}
| WFError {writeError :: String}
$(JQ.deriveToJSON (sumTypeJSON $ dropPrefix "WF") ''WriteFileResult)
cChatWriteFile :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CJSONString
cChatWriteFile cc cPath ptr len = do
c <- deRefStablePtr cc
path <- peekCString cPath
s <- getByteString ptr len
r <- chatWriteFile c path s
newCStringFromLazyBS $ J.encode r
chatWriteFile :: ChatController -> FilePath -> ByteString -> IO WriteFileResult
chatWriteFile ChatController {random} path s = do
cfArgs <- atomically $ CF.randomArgs random
let file = CryptoFile path $ Just cfArgs
either WFError (\_ -> WFResult cfArgs)
<$> runCatchExceptT (withExceptT show $ CF.writeFile file $ LB.fromStrict s)
data ReadFileResult
= RFResult {fileSize :: Int}
| RFError {readError :: String}
cChatReadFile :: CString -> CString -> CString -> IO (Ptr Word8)
cChatReadFile cPath cKey cNonce = do
path <- peekCString cPath
key <- B.packCString cKey
nonce <- B.packCString cNonce
chatReadFile path key nonce >>= \case
Left e -> castPtr <$> newCString (chr 1 : e)
Right s -> do
let len = fromIntegral $ LB.length s
ptr <- mallocBytes $ len + 5
poke ptr (0 :: Word8)
pokeByteOff ptr 1 (fromIntegral len :: Word32)
putLazyByteString (ptr `plusPtr` 5) s
pure ptr
chatReadFile :: FilePath -> ByteString -> ByteString -> IO (Either String LB.ByteString)
chatReadFile path keyStr nonceStr = runCatchExceptT $ do
key <- liftEither $ strDecode keyStr
nonce <- liftEither $ strDecode nonceStr
let file = CryptoFile path $ Just $ CFArgs key nonce
withExceptT show $ CF.readFile file
cChatEncryptFile :: StablePtr ChatController -> CString -> CString -> IO CJSONString
cChatEncryptFile cc cFromPath cToPath = do
c <- deRefStablePtr cc
fromPath <- peekCString cFromPath
toPath <- peekCString cToPath
r <- chatEncryptFile c fromPath toPath
newCAString . LB'.unpack $ J.encode r
chatEncryptFile :: ChatController -> FilePath -> FilePath -> IO WriteFileResult
chatEncryptFile ChatController {random} fromPath toPath =
either WFError WFResult <$> runCatchExceptT encrypt
where
encrypt = do
cfArgs <- atomically $ CF.randomArgs random
encryptFile fromPath toPath cfArgs
pure cfArgs
cChatDecryptFile :: CString -> CString -> CString -> CString -> IO CString
cChatDecryptFile cFromPath cKey cNonce cToPath = do
fromPath <- peekCString cFromPath
key <- B.packCString cKey
nonce <- B.packCString cNonce
toPath <- peekCString cToPath
r <- chatDecryptFile fromPath key nonce toPath
newCAString r
chatDecryptFile :: FilePath -> ByteString -> ByteString -> FilePath -> IO String
chatDecryptFile fromPath keyStr nonceStr toPath = fromLeft "" <$> runCatchExceptT decrypt
where
decrypt = do
key <- liftEither $ strDecode keyStr
nonce <- liftEither $ strDecode nonceStr
let fromFile = CryptoFile fromPath $ Just $ CFArgs key nonce
size <- liftIO $ CF.getFileContentsSize fromFile
withExceptT show $
CF.withFile fromFile ReadMode $ \r -> withFile toPath WriteMode $ \w -> do
decryptChunks r w size
CF.hGetTag r
decryptChunks :: CryptoFileHandle -> Handle -> Integer -> ExceptT FTCryptoError IO ()
decryptChunks r w !size = do
let chSize = min size chunkSize
chSize' = fromIntegral chSize
size' = size - chSize
ch <- liftIO $ CF.hGet r chSize'
when (B.length ch /= chSize') $ throwError $ FTCEFileIOError "encrypting file: unexpected EOF"
liftIO $ B.hPut w ch
when (size' > 0) $ decryptChunks r w size'
runCatchExceptT :: ExceptT String IO a -> IO (Either String a)
runCatchExceptT action = runExceptT action `catchAll` (pure . Left . show)
$(JQ.deriveToJSON (sumTypeJSON $ dropPrefix "RF") ''ReadFileResult)