core: support unicode filenames and catch IO exceptions in C API for local file encryption (#3035)

* core: support unicode filenames in C API

* catch IO exceptions and return as errors
This commit is contained in:
Evgeny Poberezkin 2023-09-07 13:44:37 +01:00 committed by GitHub
parent 7cd4a417e7
commit b5a0269aa2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 84 additions and 30 deletions

View File

@ -34,6 +34,7 @@ import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..), Cryp
import qualified Simplex.Messaging.Crypto.File as CF import qualified Simplex.Messaging.Crypto.File as CF
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
import Simplex.Messaging.Util (catchAll)
import UnliftIO (Handle, IOMode (..), withFile) import UnliftIO (Handle, IOMode (..), withFile)
data WriteFileResult data WriteFileResult
@ -45,7 +46,7 @@ instance ToJSON WriteFileResult where toEncoding = J.genericToEncoding . sumType
cChatWriteFile :: CString -> Ptr Word8 -> CInt -> IO CJSONString cChatWriteFile :: CString -> Ptr Word8 -> CInt -> IO CJSONString
cChatWriteFile cPath ptr len = do cChatWriteFile cPath ptr len = do
path <- peekCAString cPath path <- peekCString cPath
s <- getByteString ptr len s <- getByteString ptr len
r <- chatWriteFile path s r <- chatWriteFile path s
newCAString $ LB'.unpack $ J.encode r newCAString $ LB'.unpack $ J.encode r
@ -54,8 +55,8 @@ chatWriteFile :: FilePath -> ByteString -> IO WriteFileResult
chatWriteFile path s = do chatWriteFile path s = do
cfArgs <- CF.randomArgs cfArgs <- CF.randomArgs
let file = CryptoFile path $ Just cfArgs let file = CryptoFile path $ Just cfArgs
either (WFError . show) (\_ -> WFResult cfArgs) either WFError (\_ -> WFResult cfArgs)
<$> runExceptT (CF.writeFile file $ LB.fromStrict s) <$> runCatchExceptT (withExceptT show $ CF.writeFile file $ LB.fromStrict s)
data ReadFileResult data ReadFileResult
= RFResult {fileSize :: Int} = RFResult {fileSize :: Int}
@ -66,7 +67,7 @@ instance ToJSON ReadFileResult where toEncoding = J.genericToEncoding . sumTypeJ
cChatReadFile :: CString -> CString -> CString -> IO (Ptr Word8) cChatReadFile :: CString -> CString -> CString -> IO (Ptr Word8)
cChatReadFile cPath cKey cNonce = do cChatReadFile cPath cKey cNonce = do
path <- peekCAString cPath path <- peekCString cPath
key <- B.packCString cKey key <- B.packCString cKey
nonce <- B.packCString cNonce nonce <- B.packCString cNonce
(r, s) <- chatReadFile path key nonce (r, s) <- chatReadFile path key nonce
@ -78,7 +79,7 @@ cChatReadFile cPath cKey cNonce = do
chatReadFile :: FilePath -> ByteString -> ByteString -> IO (ReadFileResult, ByteString) chatReadFile :: FilePath -> ByteString -> ByteString -> IO (ReadFileResult, ByteString)
chatReadFile path keyStr nonceStr = do chatReadFile path keyStr nonceStr = do
either ((,"") . RFError) result <$> runExceptT readFile_ either ((,"") . RFError) result <$> runCatchExceptT readFile_
where where
result s = let s' = LB.toStrict s in (RFResult $ B.length s', s') result s = let s' = LB.toStrict s in (RFResult $ B.length s', s')
readFile_ :: ExceptT String IO LB.ByteString readFile_ :: ExceptT String IO LB.ByteString
@ -90,14 +91,14 @@ chatReadFile path keyStr nonceStr = do
cChatEncryptFile :: CString -> CString -> IO CJSONString cChatEncryptFile :: CString -> CString -> IO CJSONString
cChatEncryptFile cFromPath cToPath = do cChatEncryptFile cFromPath cToPath = do
fromPath <- peekCAString cFromPath fromPath <- peekCString cFromPath
toPath <- peekCAString cToPath toPath <- peekCString cToPath
r <- chatEncryptFile fromPath toPath r <- chatEncryptFile fromPath toPath
newCAString . LB'.unpack $ J.encode r newCAString . LB'.unpack $ J.encode r
chatEncryptFile :: FilePath -> FilePath -> IO WriteFileResult chatEncryptFile :: FilePath -> FilePath -> IO WriteFileResult
chatEncryptFile fromPath toPath = chatEncryptFile fromPath toPath =
either WFError WFResult <$> runExceptT encrypt either WFError WFResult <$> runCatchExceptT encrypt
where where
encrypt = do encrypt = do
cfArgs <- liftIO $ CF.randomArgs cfArgs <- liftIO $ CF.randomArgs
@ -114,15 +115,15 @@ chatEncryptFile fromPath toPath =
cChatDecryptFile :: CString -> CString -> CString -> CString -> IO CString cChatDecryptFile :: CString -> CString -> CString -> CString -> IO CString
cChatDecryptFile cFromPath cKey cNonce cToPath = do cChatDecryptFile cFromPath cKey cNonce cToPath = do
fromPath <- peekCAString cFromPath fromPath <- peekCString cFromPath
key <- B.packCString cKey key <- B.packCString cKey
nonce <- B.packCString cNonce nonce <- B.packCString cNonce
toPath <- peekCAString cToPath toPath <- peekCString cToPath
r <- chatDecryptFile fromPath key nonce toPath r <- chatDecryptFile fromPath key nonce toPath
newCAString r newCAString r
chatDecryptFile :: FilePath -> ByteString -> ByteString -> FilePath -> IO String chatDecryptFile :: FilePath -> ByteString -> ByteString -> FilePath -> IO String
chatDecryptFile fromPath keyStr nonceStr toPath = fromLeft "" <$> runExceptT decrypt chatDecryptFile fromPath keyStr nonceStr toPath = fromLeft "" <$> runCatchExceptT decrypt
where where
decrypt = do decrypt = do
key <- liftEither $ strDecode keyStr key <- liftEither $ strDecode keyStr
@ -143,6 +144,9 @@ chatDecryptFile fromPath keyStr nonceStr toPath = fromLeft "" <$> runExceptT dec
liftIO $ B.hPut w ch liftIO $ B.hPut w ch
when (size' > 0) $ decryptChunks r w size' when (size' > 0) $ decryptChunks r w size'
runCatchExceptT :: ExceptT String IO a -> IO (Either String a)
runCatchExceptT action = runExceptT action `catchAll` (pure . Left . show)
chunkSize :: Num a => a chunkSize :: Num a => a
chunkSize = 65536 chunkSize = 65536
{-# INLINE chunkSize #-} {-# INLINE chunkSize #-}

View File

@ -18,6 +18,7 @@ import Data.Word (Word8)
import Foreign.C import Foreign.C
import Foreign.Marshal.Alloc (mallocBytes) import Foreign.Marshal.Alloc (mallocBytes)
import Foreign.Ptr import Foreign.Ptr
import GHC.IO.Encoding (setLocaleEncoding, setFileSystemEncoding, setForeignEncoding)
import Simplex.Chat.Mobile import Simplex.Chat.Mobile
import Simplex.Chat.Mobile.File import Simplex.Chat.Mobile.File
import Simplex.Chat.Mobile.Shared import Simplex.Chat.Mobile.Shared
@ -27,21 +28,36 @@ import Simplex.Chat.Store.Profiles
import Simplex.Chat.Types (AgentUserId (..), Profile (..)) import Simplex.Chat.Types (AgentUserId (..), Profile (..))
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..)) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..))
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile(..), CryptoFileArgs (..), getFileContentsSize) import Simplex.Messaging.Crypto.File (CryptoFile(..), CryptoFileArgs (..))
import qualified Simplex.Messaging.Crypto.File as CF
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
import System.Directory (copyFile)
import System.FilePath ((</>)) import System.FilePath ((</>))
import System.IO (utf8)
import Test.Hspec import Test.Hspec
mobileTests :: HasCallStack => SpecWith FilePath mobileTests :: HasCallStack => SpecWith FilePath
mobileTests = do mobileTests = do
describe "mobile API" $ do describe "mobile API" $ do
runIO $ do
setLocaleEncoding utf8
setFileSystemEncoding utf8
setForeignEncoding utf8
it "start new chat without user" testChatApiNoUser it "start new chat without user" testChatApiNoUser
it "start new chat with existing user" testChatApi it "start new chat with existing user" testChatApi
it "should encrypt/decrypt WebRTC frames" testMediaApi it "should encrypt/decrypt WebRTC frames" testMediaApi
it "should encrypt/decrypt WebRTC frames via C API" testMediaCApi it "should encrypt/decrypt WebRTC frames via C API" testMediaCApi
it "should read/write encrypted files via C API" testFileCApi describe "should read/write encrypted files via C API" $ do
it "should encrypt/decrypt files via C API" testFileEncryptionCApi it "latin1 name" $ testFileCApi "test"
it "utf8 name 1" $ testFileCApi "тест"
it "utf8 name 2" $ testFileCApi "👍"
it "no exception on missing file" testMissingFileCApi
describe "should encrypt/decrypt files via C API" $ do
it "latin1 name" $ testFileEncryptionCApi "test"
it "utf8 name 1" $ testFileEncryptionCApi "тест"
it "utf8 name 2" $ testFileEncryptionCApi "👍"
it "no exception on missing file" testMissingFileEncryptionCApi
noActiveUser :: String noActiveUser :: String
#if defined(darwin_HOST_OS) && defined(swiftJSON) #if defined(darwin_HOST_OS) && defined(swiftJSON)
@ -176,16 +192,19 @@ instance FromJSON WriteFileResult where parseJSON = J.genericParseJSON . sumType
instance FromJSON ReadFileResult where parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "RF" instance FromJSON ReadFileResult where parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "RF"
testFileCApi :: FilePath -> IO () testFileCApi :: FilePath -> FilePath -> IO ()
testFileCApi tmp = do testFileCApi fileName tmp = do
src <- B.readFile "./tests/fixtures/test.pdf" src <- B.readFile "./tests/fixtures/test.pdf"
cPath <- newCAString $ tmp </> "test.pdf" let path = tmp </> (fileName <> ".pdf")
cPath <- newCString path
let len = B.length src let len = B.length src
cLen = fromIntegral len cLen = fromIntegral len
ptr <- mallocBytes $ B.length src ptr <- mallocBytes $ B.length src
putByteString ptr src putByteString ptr src
r <- peekCAString =<< cChatWriteFile cPath ptr cLen r <- peekCAString =<< cChatWriteFile cPath ptr cLen
Just (WFResult (CFArgs key nonce)) <- jDecode r Just (WFResult cfArgs@(CFArgs key nonce)) <- jDecode r
let encryptedFile = CryptoFile path $ Just cfArgs
CF.getFileContentsSize encryptedFile `shouldReturn` fromIntegral (B.length src)
cKey <- encodedCString key cKey <- encodedCString key
cNonce <- encodedCString nonce cNonce <- encodedCString nonce
ptr' <- cChatReadFile cPath cKey cNonce ptr' <- cChatReadFile cPath cKey cNonce
@ -196,22 +215,53 @@ testFileCApi tmp = do
contents `shouldBe` src contents `shouldBe` src
sz `shouldBe` len sz `shouldBe` len
testFileEncryptionCApi :: FilePath -> IO () testMissingFileCApi :: FilePath -> IO ()
testFileEncryptionCApi tmp = do testMissingFileCApi tmp = do
src <- B.readFile "./tests/fixtures/test.pdf" let path = tmp </> "missing_file"
cFromPath <- newCAString "./tests/fixtures/test.pdf" cPath <- newCString path
let toPath = tmp </> "test.encrypted.pdf" CFArgs key nonce <- CF.randomArgs
cToPath <- newCAString toPath
r <- peekCAString =<< cChatEncryptFile cFromPath cToPath
Just (WFResult cfArgs@(CFArgs key nonce)) <- jDecode r
getFileContentsSize (CryptoFile toPath $ Just cfArgs) `shouldReturn` fromIntegral (B.length src)
cKey <- encodedCString key cKey <- encodedCString key
cNonce <- encodedCString nonce cNonce <- encodedCString nonce
let toPath' = tmp </> "test.decrypted.pdf" ptr <- cChatReadFile cPath cKey cNonce
cToPath' <- newCAString toPath' r <- peekCAString $ castPtr ptr
Just (RFError err) <- jDecode r
err `shouldContain` "missing_file: openBinaryFile: does not exist"
testFileEncryptionCApi :: FilePath -> FilePath -> IO ()
testFileEncryptionCApi fileName tmp = do
let fromPath = tmp </> (fileName <> ".source.pdf")
copyFile "./tests/fixtures/test.pdf" fromPath
src <- B.readFile fromPath
cFromPath <- newCString fromPath
let toPath = tmp </> (fileName <> ".encrypted.pdf")
cToPath <- newCString toPath
r <- peekCAString =<< cChatEncryptFile cFromPath cToPath
Just (WFResult cfArgs@(CFArgs key nonce)) <- jDecode r
CF.getFileContentsSize (CryptoFile toPath $ Just cfArgs) `shouldReturn` fromIntegral (B.length src)
cKey <- encodedCString key
cNonce <- encodedCString nonce
let toPath' = tmp </> (fileName <> ".decrypted.pdf")
cToPath' <- newCString toPath'
"" <- peekCAString =<< cChatDecryptFile cToPath cKey cNonce cToPath' "" <- peekCAString =<< cChatDecryptFile cToPath cKey cNonce cToPath'
B.readFile toPath' `shouldReturn` src B.readFile toPath' `shouldReturn` src
testMissingFileEncryptionCApi :: FilePath -> IO ()
testMissingFileEncryptionCApi tmp = do
let fromPath = tmp </> "missing_file.source.pdf"
toPath = tmp </> "missing_file.encrypted.pdf"
cFromPath <- newCString fromPath
cToPath <- newCString toPath
r <- peekCAString =<< cChatEncryptFile cFromPath cToPath
Just (WFError err) <- jDecode r
err `shouldContain` fromPath
CFArgs key nonce <- CF.randomArgs
cKey <- encodedCString key
cNonce <- encodedCString nonce
let toPath' = tmp </> "missing_file.decrypted.pdf"
cToPath' <- newCString toPath'
err' <- peekCAString =<< cChatDecryptFile cToPath cKey cNonce cToPath'
err' `shouldContain` toPath
jDecode :: FromJSON a => String -> IO (Maybe a) jDecode :: FromJSON a => String -> IO (Maybe a)
jDecode = pure . J.decode . LB.pack jDecode = pure . J.decode . LB.pack