From 02ca7234fbcd8be6cc85b8e392274dc5f519823a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 30 Aug 2022 12:49:07 +0100 Subject: [PATCH 01/44] use SQLCipher (#981) * use SQLCipher * pass encryption key via CLI options * update dependencies to use git * add CONTRIBUTING.md * move flag, enable build in sqlcipher branch * update dependencies --- .github/workflows/build.yml | 5 +++-- cabal.project | 17 +++++++++++++- docs/CONTRIBUTING.md | 16 +++++++++++++ docs/rfcs/2022-08-29-database-encryption.md | 25 +++++++++++++++++++++ package.yaml | 2 +- scripts/nix/sha256map.nix | 4 +++- simplex-chat.cabal | 10 ++++----- src/Simplex/Chat.hs | 8 +++---- src/Simplex/Chat/Core.hs | 2 +- src/Simplex/Chat/Mobile.hs | 16 +++++++++++-- src/Simplex/Chat/Options.hs | 10 +++++++++ src/Simplex/Chat/Store.hs | 4 ++-- stack.yaml | 8 ++++++- tests/ChatClient.hs | 10 +++++---- tests/MobileTests.hs | 2 +- tests/SchemaDump.hs | 2 +- 16 files changed, 115 insertions(+), 26 deletions(-) create mode 100644 docs/CONTRIBUTING.md create mode 100644 docs/rfcs/2022-08-29-database-encryption.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e16f719cb0..f5984b70b0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,7 @@ on: branches: - master - stable + - sqlcipher tags: - "v*" pull_request: @@ -67,9 +68,9 @@ jobs: - name: Setup Stack uses: haskell/actions/setup@v1 with: - ghc-version: '8.10.7' + ghc-version: "8.10.7" enable-stack: true - stack-version: 'latest' + stack-version: "latest" - name: Cache dependencies uses: actions/cache@v2 diff --git a/cabal.project b/cabal.project index 27b2d7cb31..7ae27bac52 100644 --- a/cabal.project +++ b/cabal.project @@ -1,11 +1,26 @@ packages: . +-- packages: . ../simplexmq +-- packages: . ../simplexmq ../direct-sqlcipher ../sqlcipher-simple constraints: zip +disable-bzip2 +disable-zstd +package direct-sqlcipher + flags: +openssl + source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: a7b39b710c3aab9b2a38bd6841e52e0342b3a7ef + tag: e4b77ed9e68373e2bad48a7c825db3860a6ad4d6 + +source-repository-package + type: git + location: https://github.com/simplex-chat/direct-sqlcipher.git + tag: 477955063df65a2776c2a958b656ff359b76374d + +source-repository-package + type: git + location: https://github.com/simplex-chat/sqlcipher-simple.git + tag: 0738c7957e971b84a2a156d297596206b948c4f6 source-repository-package type: git diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000000..9ae8ec9512 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,16 @@ +# Contributing guide + +## Compiling with SQLCipher encryption enabled + +Add `cabal.project.local` to project root with the location of OpenSSL headers and libraries and flag setting encryption mode: + +``` +ignore-project: False + +package direct-sqlcipher + extra-include-dirs: /opt/homebrew/opt/openssl@3/include + extra-lib-dirs: /opt/homebrew/opt/openssl@3/lib + flags: +openssl +``` + +OpenSSL can be installed with `brew install openssl` diff --git a/docs/rfcs/2022-08-29-database-encryption.md b/docs/rfcs/2022-08-29-database-encryption.md new file mode 100644 index 0000000000..367201ab97 --- /dev/null +++ b/docs/rfcs/2022-08-29-database-encryption.md @@ -0,0 +1,25 @@ +# Database encryption + +## Approach + +Using SQLCipher - it is a drop in replacement for SQLite that works for non-encrypted databases without any changes (TODO test on iOS/Android). + +`direct-sqlite` and `sqlite-simple` libraries are forked and renamed to `direct-sqlcipher` and `sqlcipher-simple`, with replaced cbits in `direct-sqlcipher` (TODO include SQLCipher as git submodule with a script to upgrade cbits). + +While SQLCipher provides additional C functions to set and change database key, they do not necessarily need to be exported as they are available as PRAGMAs. + +Moving from plaintext to encrypted database (and back) requires migration process using [sqlcipher_export() function](https://discuss.zetetic.net/t/how-to-encrypt-a-plaintext-sqlite-database-to-use-sqlcipher-and-avoid-file-is-encrypted-or-is-not-a-database-errors/868). + +The approach would be similar to database migration for the notifications: + +1. the current users will be offered to migrate to encrypted database once, with a notice that it can be done later via settings. +2. the new users will be asked to enter a pass-phrase to create a new database (it can be empty, in which case the database won't be encrypted). +3. during the migration the database backup will be created and the old database files will be preserved - in case of the app failing to open the new database right after the migration it should revert to using the previous database. + +When opening the database the key must be passed via chat command / agent configuration, some test query must be performed to check that the key is correct: https://www.zetetic.net/sqlcipher/sqlcipher-api/#PRAGMA_key + +Options to support in chat settings: + +- encrypt database (with automatic rollback in case of failure) +- decrypt database (-"-) +- change key (using [PRAGMA rekey](https://www.zetetic.net/sqlcipher/sqlcipher-api/#rekey)) diff --git a/package.yaml b/package.yaml index 893402be70..3980d371b7 100644 --- a/package.yaml +++ b/package.yaml @@ -35,7 +35,7 @@ dependencies: - simple-logger == 0.1.* - simplexmq >= 3.0 - socks == 0.6.* - - sqlite-simple == 0.4.* + - sqlcipher-simple == 0.4.* - stm == 2.5.* - terminal == 0.2.* - text == 1.2.* diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 76ea520034..4dcc510f0e 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,7 @@ { - "https://github.com/simplex-chat/simplexmq.git"."a7b39b710c3aab9b2a38bd6841e52e0342b3a7ef" = "0iqk58dhckpij9l1z8bm83hghw5cwj9hmpkbk7j8vws123g1bd73"; + "https://github.com/simplex-chat/simplexmq.git"."e4b77ed9e68373e2bad48a7c825db3860a6ad4d6" = "07p8g0a0pl61wrai2jyn311ys238s9kl1i98kpxsjifqif1h9wc1"; + "https://github.com/simplex-chat/direct-sqlcipher.git"."477955063df65a2776c2a958b656ff359b76374d" = "1xiqid1344mwh3wnrczn6rxf59hml5g7kifah7skpd9javj4bb7s"; + "https://github.com/simplex-chat/sqlcipher-simple.git"."0738c7957e971b84a2a156d297596206b948c4f6" = "0lysvzx2qzjcxka9w5cb0bnzym3nrqh7r7q5dw9h6g46vybc5lyc"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 662df81c05..9e3e80aedc 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -90,7 +90,7 @@ library , simple-logger ==0.1.* , simplexmq >=3.0 , socks ==0.6.* - , sqlite-simple ==0.4.* + , sqlcipher-simple ==0.4.* , stm ==2.5.* , terminal ==0.2.* , text ==1.2.* @@ -132,7 +132,7 @@ executable simplex-bot , simplex-chat , simplexmq >=3.0 , socks ==0.6.* - , sqlite-simple ==0.4.* + , sqlcipher-simple ==0.4.* , stm ==2.5.* , terminal ==0.2.* , text ==1.2.* @@ -174,7 +174,7 @@ executable simplex-bot-advanced , simplex-chat , simplexmq >=3.0 , socks ==0.6.* - , sqlite-simple ==0.4.* + , sqlcipher-simple ==0.4.* , stm ==2.5.* , terminal ==0.2.* , text ==1.2.* @@ -217,7 +217,7 @@ executable simplex-chat , simplex-chat , simplexmq >=3.0 , socks ==0.6.* - , sqlite-simple ==0.4.* + , sqlcipher-simple ==0.4.* , stm ==2.5.* , terminal ==0.2.* , text ==1.2.* @@ -269,7 +269,7 @@ test-suite simplex-chat-test , simplex-chat , simplexmq >=3.0 , socks ==0.6.* - , sqlite-simple ==0.4.* + , sqlcipher-simple ==0.4.* , stm ==2.5.* , terminal ==0.2.* , text ==1.2.* diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index de5e74fa62..896bc49f2c 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -86,6 +86,7 @@ defaultChatConfig = defaultAgentConfig { tcpPort = undefined, -- agent does not listen to TCP dbFile = "simplex_v1", + dbKey = "", yesToMigrations = False }, yesToMigrations = False, @@ -124,7 +125,7 @@ logCfg :: LogConfig logCfg = LogConfig {lc_file = Nothing, lc_stderr = True} newChatController :: SQLiteStore -> Maybe User -> ChatConfig -> ChatOpts -> Maybe (Notification -> IO ()) -> IO ChatController -newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, defaultServers} ChatOpts {dbFilePrefix, smpServers, networkConfig, logConnections, logServerHosts} sendToast = do +newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, defaultServers} ChatOpts {dbFilePrefix, dbKey, smpServers, networkConfig, logConnections, logServerHosts} sendToast = do let f = chatStoreFile dbFilePrefix config = cfg {subscriptionEvents = logConnections, hostEvents = logServerHosts} sendNotification = fromMaybe (const $ pure ()) sendToast @@ -132,7 +133,7 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, de firstTime <- not <$> doesFileExist f currentUser <- newTVarIO user servers <- resolveServers defaultServers - smpAgent <- getSMPAgentClient aCfg {dbFile = dbFilePrefix <> "_agent.db"} servers {netCfg = networkConfig} + smpAgent <- getSMPAgentClient aCfg {dbFile = dbFilePrefix <> "_agent.db", dbKey} servers {netCfg = networkConfig} agentAsync <- newTVarIO Nothing idsDrg <- newTVarIO =<< drgNew inputQ <- newTBQueueIO tbqSize @@ -1715,8 +1716,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage (probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId ct void . sendDirectContactMessage ct $ XInfoProbe probe if connectedIncognito - then - withStore' $ \db -> deleteSentProbe db userId probeId + then withStore' $ \db -> deleteSentProbe db userId probeId else do cs <- withStore' $ \db -> getMatchingContacts db userId ct let probeHash = ProbeHash $ C.sha256Hash (unProbe probe) diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index 597b524e27..99097d7e07 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -23,7 +23,7 @@ simplexChatCore cfg@ChatConfig {yesToMigrations} opts sendToast chat where initRun = do let f = chatStoreFile $ dbFilePrefix opts - st <- createStore f yesToMigrations + st <- createStore f (dbKey opts) yesToMigrations u <- getCreateActiveUser st cc <- newChatController st (Just u) cfg opts sendToast runSimplexChat opts u cc chat diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 5e862f315c..03cbf055e1 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -31,6 +31,8 @@ import System.Timeout (timeout) foreign export ccall "chat_init" cChatInit :: CString -> IO (StablePtr ChatController) +foreign export ccall "chat_init_key" cChatInitKey :: CString -> CString -> IO (StablePtr ChatController) + foreign export ccall "chat_send_cmd" cChatSendCmd :: StablePtr ChatController -> CString -> IO CJSONString foreign export ccall "chat_recv_msg" cChatRecvMsg :: StablePtr ChatController -> IO CJSONString @@ -44,6 +46,12 @@ foreign export ccall "chat_parse_markdown" cChatParseMarkdown :: CString -> IO C cChatInit :: CString -> IO (StablePtr ChatController) cChatInit fp = peekCAString fp >>= chatInit >>= newStablePtr +-- | initialize chat controller with encrypted database +-- The active user has to be created and the chat has to be started before most commands can be used. +cChatInitKey :: CString -> CString -> IO (StablePtr ChatController) +cChatInitKey fp key = + ((,) <$> peekCAString fp <*> peekCAString key) >>= uncurry chatInitKey >>= newStablePtr + -- | send command to chat (same syntax as in terminal for now) cChatSendCmd :: StablePtr ChatController -> CString -> IO CJSONString cChatSendCmd cPtr cCmd = do @@ -67,6 +75,7 @@ mobileChatOpts :: ChatOpts mobileChatOpts = ChatOpts { dbFilePrefix = undefined, + dbKey = "", smpServers = [], networkConfig = defaultNetworkConfig, logConnections = False, @@ -91,9 +100,12 @@ getActiveUser_ :: SQLiteStore -> IO (Maybe User) getActiveUser_ st = find activeUser <$> withTransaction st getUsers chatInit :: String -> IO ChatController -chatInit dbFilePrefix = do +chatInit = (`chatInitKey` "") + +chatInitKey :: String -> String -> IO ChatController +chatInitKey dbFilePrefix dbKey = do let f = chatStoreFile dbFilePrefix - chatStore <- createStore f (yesToMigrations (defaultMobileConfig :: ChatConfig)) + chatStore <- createStore f dbKey (yesToMigrations (defaultMobileConfig :: ChatConfig)) user_ <- getActiveUser_ chatStore newChatController chatStore user_ defaultMobileConfig mobileChatOpts {dbFilePrefix} Nothing diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index 24f809a4a1..8f2bbfd0f5 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -25,6 +25,7 @@ import System.FilePath (combine) data ChatOpts = ChatOpts { dbFilePrefix :: String, + dbKey :: String, smpServers :: [SMPServer], networkConfig :: NetworkConfig, logConnections :: Bool, @@ -47,6 +48,14 @@ chatOpts appDir defaultDbFileName = do <> value defaultDbFilePath <> showDefault ) + dbKey <- + strOption + ( long "key" + <> short 'k' + <> metavar "KEY" + <> help "Database encryption key/pass-phrase" + <> value "" + ) smpServers <- option parseSMPServers @@ -126,6 +135,7 @@ chatOpts appDir defaultDbFileName = do pure ChatOpts { dbFilePrefix, + dbKey, smpServers, networkConfig = fullNetworkConfig socksProxy $ useTcpTimeout socksProxy t, logConnections, diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index e4391d651c..56ca84a1b2 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -276,8 +276,8 @@ migrations = sortBy (compare `on` name) $ map migration schemaMigrations where migration (name, query) = Migration {name = name, up = fromQuery query} -createStore :: FilePath -> Bool -> IO SQLiteStore -createStore dbFilePath = createSQLiteStore dbFilePath migrations +createStore :: FilePath -> String -> Bool -> IO SQLiteStore +createStore dbFilePath dbKey = createSQLiteStore dbFilePath dbKey migrations chatStoreFile :: FilePath -> FilePath chatStoreFile = (<> "_chat.db") diff --git a/stack.yaml b/stack.yaml index 4b4865916f..a0b5d5439e 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,13 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: a7b39b710c3aab9b2a38bd6841e52e0342b3a7ef + commit: e4b77ed9e68373e2bad48a7c825db3860a6ad4d6 + # - ../direct-sqlcipher + - github: simplex-chat/direct-sqlcipher + commit: 477955063df65a2776c2a958b656ff359b76374d + # - ../sqlcipher-simple + - github: simplex-chat/sqlcipher-simple + commit: 0738c7957e971b84a2a156d297596206b948c4f6 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 6edb5e99f8..40e12b1835 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -48,6 +48,8 @@ testOpts :: ChatOpts testOpts = ChatOpts { dbFilePrefix = undefined, + dbKey = "", + -- dbKey = "this is a pass-phrase to encrypt the database", smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001"], networkConfig = defaultNetworkConfig, logConnections = False, @@ -101,16 +103,16 @@ testCfgV1 :: ChatConfig testCfgV1 = testCfg {agentConfig = testAgentCfgV1} createTestChat :: ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC -createTestChat cfg opts dbPrefix profile = do +createTestChat cfg opts@ChatOpts {dbKey} dbPrefix profile = do let dbFilePrefix = testDBPrefix <> dbPrefix - st <- createStore (dbFilePrefix <> "_chat.db") False + st <- createStore (dbFilePrefix <> "_chat.db") dbKey False Right user <- withTransaction st $ \db -> runExceptT $ createUser db profile True startTestChat_ st cfg opts dbFilePrefix user startTestChat :: ChatConfig -> ChatOpts -> String -> IO TestCC -startTestChat cfg opts dbPrefix = do +startTestChat cfg opts@ChatOpts {dbKey} dbPrefix = do let dbFilePrefix = testDBPrefix <> dbPrefix - st <- createStore (dbFilePrefix <> "_chat.db") False + st <- createStore (dbFilePrefix <> "_chat.db") dbKey False Just user <- find activeUser <$> withTransaction st getUsers startTestChat_ st cfg opts dbFilePrefix user diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index 68057b5dc4..fd66923a65 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -82,7 +82,7 @@ testChatApiNoUser = withTmpFiles $ do testChatApi :: IO () testChatApi = withTmpFiles $ do let f = chatStoreFile $ testDBPrefix <> "1" - st <- createStore f True + st <- createStore f "" True Right _ <- withTransaction st $ \db -> runExceptT $ createUser db aliceProfile True cc <- chatInit $ testDBPrefix <> "1" chatSendCmd cc "/u" `shouldReturn` activeUser diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index 923d735314..7140846fbb 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -22,7 +22,7 @@ schemaDumpTest = testVerifySchemaDump :: IO () testVerifySchemaDump = withTmpFiles $ do - void $ createStore testDB False + void $ createStore testDB "" False void $ readCreateProcess (shell $ "touch " <> schema) "" savedSchema <- readFile schema savedSchema `deepseq` pure () From 025f838f4367e861245fbb3c213f9c0338ff2ce9 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 30 Aug 2022 14:33:43 +0100 Subject: [PATCH 02/44] update dependencies --- cabal.project | 6 +++--- scripts/nix/sha256map.nix | 6 +++--- stack.yaml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cabal.project b/cabal.project index 7ae27bac52..e83efee88d 100644 --- a/cabal.project +++ b/cabal.project @@ -10,17 +10,17 @@ package direct-sqlcipher source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: e4b77ed9e68373e2bad48a7c825db3860a6ad4d6 + tag: f872c25f09393c191913bf658f1721aeaf6443e4 source-repository-package type: git location: https://github.com/simplex-chat/direct-sqlcipher.git - tag: 477955063df65a2776c2a958b656ff359b76374d + tag: 34309410eb2069b029b8fc1872deb1e0db123294 source-repository-package type: git location: https://github.com/simplex-chat/sqlcipher-simple.git - tag: 0738c7957e971b84a2a156d297596206b948c4f6 + tag: 5e154a2aeccc33ead6c243ec07195ab673137221 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 4dcc510f0e..bbb4d43c65 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,7 +1,7 @@ { - "https://github.com/simplex-chat/simplexmq.git"."e4b77ed9e68373e2bad48a7c825db3860a6ad4d6" = "07p8g0a0pl61wrai2jyn311ys238s9kl1i98kpxsjifqif1h9wc1"; - "https://github.com/simplex-chat/direct-sqlcipher.git"."477955063df65a2776c2a958b656ff359b76374d" = "1xiqid1344mwh3wnrczn6rxf59hml5g7kifah7skpd9javj4bb7s"; - "https://github.com/simplex-chat/sqlcipher-simple.git"."0738c7957e971b84a2a156d297596206b948c4f6" = "0lysvzx2qzjcxka9w5cb0bnzym3nrqh7r7q5dw9h6g46vybc5lyc"; + "https://github.com/simplex-chat/simplexmq.git"."f872c25f09393c191913bf658f1721aeaf6443e4" = "0gfy431imcnxb0mll6skka30hvj1fwqhywdxs7nibkm9dnhzj05a"; + "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; + "https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index a0b5d5439e..c73a29aa98 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,13 +49,13 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: e4b77ed9e68373e2bad48a7c825db3860a6ad4d6 + commit: f872c25f09393c191913bf658f1721aeaf6443e4 # - ../direct-sqlcipher - github: simplex-chat/direct-sqlcipher - commit: 477955063df65a2776c2a958b656ff359b76374d + commit: 34309410eb2069b029b8fc1872deb1e0db123294 # - ../sqlcipher-simple - github: simplex-chat/sqlcipher-simple - commit: 0738c7957e971b84a2a156d297596206b948c4f6 + commit: 5e154a2aeccc33ead6c243ec07195ab673137221 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 5e5c851173b96ad0fee168cff48fdfe13162088d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 30 Aug 2022 16:35:56 +0100 Subject: [PATCH 03/44] update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index e83efee88d..25ecd35401 100644 --- a/cabal.project +++ b/cabal.project @@ -10,7 +10,7 @@ package direct-sqlcipher source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: f872c25f09393c191913bf658f1721aeaf6443e4 + tag: c66a7e371f4e9ac79237a7042c76426a6a068899 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index bbb4d43c65..a2debf4e12 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."f872c25f09393c191913bf658f1721aeaf6443e4" = "0gfy431imcnxb0mll6skka30hvj1fwqhywdxs7nibkm9dnhzj05a"; + "https://github.com/simplex-chat/simplexmq.git"."c66a7e371f4e9ac79237a7042c76426a6a068899" = "0pz2px1n108nfbqy0d8cgqx5230j17jhycyprbsky5ywsfhpahbv"; "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; "https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; diff --git a/stack.yaml b/stack.yaml index c73a29aa98..f33911bcb9 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: f872c25f09393c191913bf658f1721aeaf6443e4 + commit: c66a7e371f4e9ac79237a7042c76426a6a068899 # - ../direct-sqlcipher - github: simplex-chat/direct-sqlcipher commit: 34309410eb2069b029b8fc1872deb1e0db123294 From 3613fc953e4bbe1bb4b36a14178b21321fef8b1f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 31 Aug 2022 18:07:34 +0100 Subject: [PATCH 04/44] core: encrypt chat database (#988) * core: encrypt chat database * check DB key error on start * function to encrypt database * encrypt database command * decrypt, rekey * remove rekey, refactor * test for db encryption/decryption * update simplexmq --- cabal.project | 2 +- package.yaml | 1 + scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 5 +++ src/Simplex/Chat.hs | 21 ++++++---- src/Simplex/Chat/Archive.hs | 77 ++++++++++++++++++++++++++++++---- src/Simplex/Chat/Controller.hs | 17 ++++++++ src/Simplex/Chat/Terminal.hs | 13 +++++- src/Simplex/Chat/View.hs | 5 ++- stack.yaml | 2 +- tests/ChatTests.hs | 44 ++++++++++++++++++- 11 files changed, 167 insertions(+), 22 deletions(-) diff --git a/cabal.project b/cabal.project index 25ecd35401..e859fc0749 100644 --- a/cabal.project +++ b/cabal.project @@ -10,7 +10,7 @@ package direct-sqlcipher source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: c66a7e371f4e9ac79237a7042c76426a6a068899 + tag: 26d149d17c0ceb5cc17d0fd1c1357d95bd47e549 source-repository-package type: git diff --git a/package.yaml b/package.yaml index 3980d371b7..6ba036084c 100644 --- a/package.yaml +++ b/package.yaml @@ -23,6 +23,7 @@ dependencies: - containers == 0.6.* - cryptonite >= 0.27 && < 0.30 - directory == 1.3.* + - direct-sqlcipher == 2.3.* - email-validate == 2.3.* - exceptions == 0.10.* - filepath == 1.4.* diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index a2debf4e12..622500603c 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."c66a7e371f4e9ac79237a7042c76426a6a068899" = "0pz2px1n108nfbqy0d8cgqx5230j17jhycyprbsky5ywsfhpahbv"; + "https://github.com/simplex-chat/simplexmq.git"."26d149d17c0ceb5cc17d0fd1c1357d95bd47e549" = "135knaxsyag3mlml62w2j4y8shvi82q8frhcn5b28qd8hlg5q2rq"; "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; "https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 9e3e80aedc..d293ad9378 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -77,6 +77,7 @@ library , composition ==1.0.* , containers ==0.6.* , cryptonite >=0.27 && <0.30 + , direct-sqlcipher ==2.3.* , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* @@ -118,6 +119,7 @@ executable simplex-bot , composition ==1.0.* , containers ==0.6.* , cryptonite >=0.27 && <0.30 + , direct-sqlcipher ==2.3.* , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* @@ -160,6 +162,7 @@ executable simplex-bot-advanced , composition ==1.0.* , containers ==0.6.* , cryptonite >=0.27 && <0.30 + , direct-sqlcipher ==2.3.* , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* @@ -203,6 +206,7 @@ executable simplex-chat , composition ==1.0.* , containers ==0.6.* , cryptonite >=0.27 && <0.30 + , direct-sqlcipher ==2.3.* , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* @@ -254,6 +258,7 @@ test-suite simplex-chat-test , containers ==0.6.* , cryptonite >=0.27 && <0.30 , deepseq ==1.4.* + , direct-sqlcipher ==2.3.* , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 896bc49f2c..48146abdc8 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -26,7 +26,7 @@ import Data.Bifunctor (first) import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B -import Data.Char (isSpace) +import Data.Char (isSpace, ord) import Data.Either (fromRight) import Data.Fixed (div') import Data.Functor (($>)) @@ -217,11 +217,7 @@ processChatCommand = \case StartChat subConns -> withUser' $ \user -> asks agentAsync >>= readTVarIO >>= \case Just _ -> pure CRChatRunning - _ -> - ifM - (asks chatStoreChanged >>= readTVarIO) - (throwChatError CEChatStoreChanged) - (startChatController user subConns $> CRChatStarted) + _ -> checkStoreNotChanged $ startChatController user subConns $> CRChatStarted APIStopChat -> do ask >>= stopChatController pure CRChatStopped @@ -240,8 +236,10 @@ processChatCommand = \case atomically . writeTVar incognito $ onOff pure CRCmdOk APIExportArchive cfg -> checkChatStopped $ exportArchive cfg $> CRCmdOk - APIImportArchive cfg -> checkChatStopped $ importArchive cfg >> setStoreChanged $> CRCmdOk - APIDeleteStorage -> checkChatStopped $ deleteStorage >> setStoreChanged $> CRCmdOk + APIImportArchive cfg -> withStoreChanged $ importArchive cfg + APIDeleteStorage -> withStoreChanged $ deleteStorage + APIEncryptStorage key -> checkStoreNotChanged . withStoreChanged $ encryptStorage key + APIDecryptStorage -> checkStoreNotChanged $ withStoreChanged decryptStorage APIGetChats withPCC -> CRApiChats <$> withUser (\user -> withStore' $ \db -> getChatPreviews db user withPCC) APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of CTDirect -> CRApiChat . AChat SCTDirect <$> withStore (\db -> getDirectChat db user cId pagination search) @@ -939,6 +937,10 @@ processChatCommand = \case checkChatStopped a = asks agentAsync >>= readTVarIO >>= maybe a (const $ throwChatError CEChatNotStopped) setStoreChanged :: m () setStoreChanged = asks chatStoreChanged >>= atomically . (`writeTVar` True) + withStoreChanged :: m () -> m ChatResponse + withStoreChanged a = checkChatStopped $ a >> setStoreChanged $> CRCmdOk + checkStoreNotChanged :: m ChatResponse -> m ChatResponse + checkStoreNotChanged = ifM (asks chatStoreChanged >>= readTVarIO) (throwChatError CEChatStoreChanged) getSentChatItemIdByText :: User -> ChatRef -> ByteString -> m Int64 getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of CTDirect -> withStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd (safeDecodeUtf8 msg) @@ -2536,6 +2538,8 @@ chatCommandP = "/_db export " *> (APIExportArchive <$> jsonP), "/_db import " *> (APIImportArchive <$> jsonP), "/_db delete" $> APIDeleteStorage, + "/db encrypt " *> (APIEncryptStorage <$> encryptionKeyP), + "/db decrypt" $> APIDecryptStorage, "/_get chats" *> (APIGetChats <$> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False)), "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP <*> optional searchP), "/_get items count=" *> (APIGetChatItems <$> A.decimal), @@ -2685,6 +2689,7 @@ chatCommandP = t_ <- optional $ " timeout=" *> A.decimal let tcpTimeout = 1000000 * fromMaybe (maybe 5 (const 10) socksProxy) t_ pure $ fullNetworkConfig socksProxy tcpTimeout + encryptionKeyP = B.unpack <$> A.takeWhile1 (\c -> ord c >= 0x20 && ord c <= 0x7E) adminContactReq :: ConnReqContact adminContactReq = diff --git a/src/Simplex/Chat/Archive.hs b/src/Simplex/Chat/Archive.hs index 8f7a92e675..d67a76520a 100644 --- a/src/Simplex/Chat/Archive.hs +++ b/src/Simplex/Chat/Archive.hs @@ -1,16 +1,30 @@ {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} -module Simplex.Chat.Archive where +module Simplex.Chat.Archive + ( exportArchive, + importArchive, + deleteStorage, + encryptStorage, + decryptStorage, + ) +where import qualified Codec.Archive.Zip as Z +import Control.Monad.Except import Control.Monad.Reader +import qualified Data.Text as T +import qualified Database.SQLite3 as SQL import Simplex.Chat.Controller -import Simplex.Messaging.Agent.Client (agentDbPath) -import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..)) -import Simplex.Messaging.Util (whenM) +import Simplex.Messaging.Agent.Client (agentStore) +import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), sqlString) +import Simplex.Messaging.Util (unlessM, whenM) import System.FilePath import UnliftIO.Directory +import UnliftIO.Exception (SomeException, bracket, catch) import UnliftIO.STM import UnliftIO.Temporary @@ -73,14 +87,63 @@ deleteStorage = do data StorageFiles = StorageFiles { chatDb :: FilePath, + chatKey :: String, agentDb :: FilePath, + agentKey :: String, filesPath :: Maybe FilePath } storageFiles :: ChatMonad m => m StorageFiles storageFiles = do ChatController {chatStore, filesFolder, smpAgent} <- ask - let SQLiteStore {dbFilePath = chatDb} = chatStore - agentDb = agentDbPath smpAgent + let SQLiteStore {dbFilePath = chatDb, dbKey = chatKey} = chatStore + SQLiteStore {dbFilePath = agentDb, dbKey = agentKey} = agentStore smpAgent filesPath <- readTVarIO filesFolder - pure StorageFiles {chatDb, agentDb, filesPath} + pure StorageFiles {chatDb, chatKey, agentDb, agentKey, filesPath} + +encryptStorage :: forall m. ChatMonad m => String -> m () +encryptStorage key' = updateDatabase $ \f key -> export f key key' + +decryptStorage :: forall m. ChatMonad m => m () +decryptStorage = updateDatabase $ \f -> \case + "" -> throwDBError DBENotEncrypted + key -> export f key "" + +updateDatabase :: ChatMonad m => (FilePath -> String -> m ()) -> m () +updateDatabase update = do + fs@StorageFiles {chatDb, chatKey, agentDb, agentKey} <- storageFiles + checkFile `with` fs + backup `with` fs + (update chatDb chatKey >> update agentDb agentKey) + `catchError` \e -> (restore `with` fs) >> throwError e + where + action `with` StorageFiles {chatDb, agentDb} = action chatDb >> action agentDb + backup f = copyFile f (f <> ".bak") + restore f = copyFile (f <> ".bak") f + checkFile f = unlessM (doesFileExist f) $ throwDBError DBENoFile + +export :: ChatMonad m => FilePath -> String -> String -> m () +export f key key' = do + withDB (`SQL.exec` exportSQL) DBEExportFailed + renameFile (f <> ".exported") f + withDB (`SQL.exec` testSQL) DBEOpenFailed + where + withDB a err = + liftIO (bracket (SQL.open $ T.pack f) SQL.close a) + `catch` \(e :: SomeException) -> liftIO (putStrLn $ "Database error: " <> show e) >> throwDBError err + exportSQL = + T.unlines $ + keySQL key + <> [ "ATTACH DATABASE " <> sqlString (f <> ".exported") <> " AS exported KEY " <> sqlString key' <> ";", + "SELECT sqlcipher_export('exported');", + "DETACH DATABASE exported;" + ] + testSQL = + T.unlines $ + keySQL key' + <> [ "PRAGMA foreign_keys = ON;", + "PRAGMA secure_delete = ON;", + "PRAGMA auto_vacuum = FULL;", + "SELECT count(*) FROM sqlite_master;" + ] + keySQL k = ["PRAGMA key = " <> sqlString k <> ";" | not (null k)] diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 0096ae7337..92340dfb8d 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -112,6 +112,8 @@ data ChatCommand | APIExportArchive ArchiveConfig | APIImportArchive ArchiveConfig | APIDeleteStorage + | APIEncryptStorage String + | APIDecryptStorage | APIGetChats {pendingConnections :: Bool} | APIGetChat ChatRef ChatPagination (Maybe String) | APIGetChatItems Int @@ -371,6 +373,7 @@ data ChatError = ChatError {errorType :: ChatErrorType} | ChatErrorAgent {agentError :: AgentErrorType} | ChatErrorStore {storeError :: StoreError} + | ChatErrorDatabase {database :: DatabaseError} deriving (Show, Exception, Generic) instance ToJSON ChatError where @@ -428,6 +431,20 @@ instance ToJSON ChatErrorType where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CE" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CE" +data DatabaseError + = DBENotEncrypted + | DBENoFile + | DBEExportFailed + | DBEOpenFailed + deriving (Show, Exception, Generic) + +instance ToJSON DatabaseError where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "DBE" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "DBE" + +throwDBError :: ChatMonad m => DatabaseError -> m () +throwDBError = throwError . ChatErrorDatabase + type ChatMonad m = (MonadUnliftIO m, MonadReader ChatController m, MonadError ChatError m) chatCmdError :: String -> ChatResponse diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index 66a35fc138..df152df312 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -5,8 +5,11 @@ module Simplex.Chat.Terminal where +import Control.Exception (handle, throwIO) import Control.Monad.Except import qualified Data.List.NonEmpty as L +import Database.SQLite.Simple (SQLError (..)) +import qualified Database.SQLite.Simple as DB import Simplex.Chat (defaultChatConfig) import Simplex.Chat.Controller import Simplex.Chat.Core @@ -18,6 +21,7 @@ import Simplex.Chat.Terminal.Output import Simplex.Messaging.Agent.Env.SQLite (InitialAgentServers (..)) import Simplex.Messaging.Client (defaultNetworkConfig) import Simplex.Messaging.Util (raceAny_) +import System.Exit (exitFailure) terminalChatConfig :: ChatConfig terminalChatConfig = @@ -38,10 +42,17 @@ terminalChatConfig = simplexChatTerminal :: WithTerminal t => ChatConfig -> ChatOpts -> t -> IO () simplexChatTerminal cfg opts t = do sendToast <- initializeNotifications - simplexChatCore cfg opts (Just sendToast) $ \u cc -> do + handle checkDBKeyError . simplexChatCore cfg opts (Just sendToast) $ \u cc -> do ct <- newChatTerminal t when (firstTime cc) . printToTerminal ct $ chatWelcome u runChatTerminal ct cc +checkDBKeyError :: SQLError -> IO () +checkDBKeyError e = case sqlError e of + DB.ErrorNotADatabase -> do + putStrLn "Database file is invalid or you passed an incorrect encryption key" + exitFailure + _ -> throwIO e + runChatTerminal :: ChatTerminal -> ChatController -> IO () runChatTerminal ct cc = raceAny_ [runTerminalInput ct cc, runTerminalOutput ct cc, runInputLoop ct cc] diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 8e9825abb3..2e9b34c908 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -875,7 +875,7 @@ viewChatError = \case CEActiveUserExists -> ["error: active user already exists"] CEChatNotStarted -> ["error: chat not started"] CEChatNotStopped -> ["error: chat not stopped"] - CEChatStoreChanged -> ["error: chat store changed"] + CEChatStoreChanged -> ["error: chat store changed, please restart chat"] CEInvalidConnReq -> viewInvalidConnReq CEInvalidChatMessage e -> ["chat message error: " <> sShow e] CEContactNotReady c -> [ttyContact' c <> ": not ready"] @@ -932,6 +932,9 @@ viewChatError = \case SEConnectionNotFound _ -> [] -- TODO mutes delete group error, but also mutes any error from getConnectionEntity SEQuotedChatItemNotFound -> ["message not found - reply is not sent"] e -> ["chat db error: " <> sShow e] + ChatErrorDatabase err -> case err of + DBENotEncrypted -> ["error: chat database is not encrypted"] + e -> ["chat database error: " <> sShow e] ChatErrorAgent err -> case err of SMP SMP.AUTH -> [ "error: connection authorization failed - this could happen if connection was deleted,\ diff --git a/stack.yaml b/stack.yaml index f33911bcb9..0e4cde28be 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: c66a7e371f4e9ac79237a7042c76426a6a068899 + commit: 26d149d17c0ceb5cc17d0fd1c1357d95bd47e549 # - ../direct-sqlcipher - github: simplex-chat/direct-sqlcipher commit: 34309410eb2069b029b8fc1872deb1e0db123294 diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 464cdd90cc..eef6e9d8e0 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -115,6 +115,7 @@ chatTests = do describe "maintenance mode" $ do it "start/stop/export/import chat" testMaintenanceMode it "export/import chat with files" testMaintenanceModeWithFiles + it "encrypt/decrypt database" testDatabaseEncryption versionTestMatrix2 :: (TestCC -> TestCC -> IO ()) -> Spec versionTestMatrix2 runTest = do @@ -2714,7 +2715,7 @@ testMaintenanceMode = withTmpFiles $ do alice <## "ok" -- cannot start chat after import alice ##> "/_start" - alice <## "error: chat store changed" + alice <## "error: chat store changed, please restart chat" -- works after full restart withTestChat "alice" $ \alice -> testChatWorking alice bob @@ -2749,7 +2750,7 @@ testMaintenanceModeWithFiles = withTmpFiles $ do alice <## "ok" -- cannot start chat after delete alice ##> "/_start" - alice <## "error: chat store changed" + alice <## "error: chat store changed, please restart chat" doesDirectoryExist "./tests/tmp/alice_files" `shouldReturn` False alice ##> "/_db import {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}" alice <## "ok" @@ -2757,6 +2758,45 @@ testMaintenanceModeWithFiles = withTmpFiles $ do -- works after full restart withTestChat "alice" $ \alice -> testChatWorking alice bob +testDatabaseEncryption :: IO () +testDatabaseEncryption = withTmpFiles $ do + withNewTestChat "bob" bobProfile $ \bob -> do + withNewTestChatOpts testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do + alice ##> "/_start" + alice <## "chat started" + connectUsers alice bob + alice #> "@bob hi" + bob <# "alice> hi" + alice ##> "/db encrypt mykey" + alice <## "error: chat not stopped" + alice ##> "/db decrypt" + alice <## "error: chat not stopped" + alice ##> "/_stop" + alice <## "chat stopped" + alice ##> "/db decrypt" + alice <## "error: chat database is not encrypted" + alice ##> "/db encrypt mykey" + alice <## "ok" + alice ##> "/_start" + alice <## "error: chat store changed, please restart chat" + withTestChatOpts testOpts {maintenance = True, dbKey = "mykey"} "alice" $ \alice -> do + alice ##> "/_start" + alice <## "chat started" + testChatWorking alice bob + alice ##> "/_stop" + alice <## "chat stopped" + alice ##> "/db encrypt nextkey" + alice <## "ok" + withTestChatOpts testOpts {maintenance = True, dbKey = "nextkey"} "alice" $ \alice -> do + alice ##> "/_start" + alice <## "chat started" + testChatWorking alice bob + alice ##> "/_stop" + alice <## "chat stopped" + alice ##> "/db decrypt" + alice <## "ok" + withTestChat "alice" $ \alice -> testChatWorking alice bob + withTestChatContactConnected :: String -> (TestCC -> IO a) -> IO a withTestChatContactConnected dbPrefix action = withTestChat dbPrefix $ \cc -> do From 38b3965e689ca8de5ada1b8261b99db0835a6d8c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 2 Sep 2022 11:26:14 +0100 Subject: [PATCH 05/44] use commoncrypto flag in ios nix build (for sqlcipher) (#1006) * use commoncrypto flag in ios nix build (for sqlcipher) * remove openssl flag from cabal.project --- cabal.project | 3 --- flake.nix | 16 ++++++++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/cabal.project b/cabal.project index e859fc0749..de7ae67bb1 100644 --- a/cabal.project +++ b/cabal.project @@ -4,9 +4,6 @@ packages: . constraints: zip +disable-bzip2 +disable-zstd -package direct-sqlcipher - flags: +openssl - source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git diff --git a/flake.nix b/flake.nix index eab982de5d..23f22ee737 100644 --- a/flake.nix +++ b/flake.nix @@ -219,7 +219,13 @@ }; "aarch64-darwin" = { # this is the aarch64-darwin iOS build (to be patched with mac2ios) - "aarch64-darwin-ios:lib:simplex-chat" = (drv' { pkgs' = pkgs; extra-modules = [{ packages.simplexmq.flags.swift = true; }]; } ).simplex-chat.components.library.override { + "aarch64-darwin-ios:lib:simplex-chat" = (drv' { + pkgs' = pkgs; + extra-modules = [{ + packages.simplexmq.flags.swift = true; + packages.direct-sqlcipher.flags.commoncrypto = true; + }]; + } ).simplex-chat.components.library.override { smallAddressSpace = true; enableShared = false; # we need threaded here, otherwise all the queing logic doesn't work properly. # for iOS we also use -staticlib, to get one rolled up library. @@ -273,7 +279,13 @@ }; "x86_64-darwin" = { # this is the aarch64-darwin iOS build (to be patched with mac2ios) - "x86_64-darwin-ios:lib:simplex-chat" = (drv' { pkgs' = pkgs; extra-modules = [{ packages.simplexmq.flags.swift = true; }]; } ).simplex-chat.components.library.override { + "x86_64-darwin-ios:lib:simplex-chat" = (drv' { + pkgs' = pkgs; + extra-modules = [{ + packages.simplexmq.flags.swift = true; + packages.direct-sqlcipher.flags.commoncrypto = true; + }]; + } ).simplex-chat.components.library.override { smallAddressSpace = true; enableShared = false; # we need threaded here, otherwise all the queing logic doesn't work properly. # for iOS we also use -staticlib, to get one rolled up library. From 2b5e3a9459532c25fb5bb6a8a5ad9e0fb4562a22 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 2 Sep 2022 16:38:41 +0100 Subject: [PATCH 06/44] core: C API to migrate and check database (#1008) * core: C API to migrate and check database * update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 2 +- src/Simplex/Chat/Core.hs | 2 +- src/Simplex/Chat/Mobile.hs | 49 +++++++++++++++++++++++++++++++++++--- src/Simplex/Chat/Store.hs | 10 +++++--- stack.yaml | 2 +- tests/ChatClient.hs | 4 ++-- tests/MobileTests.hs | 12 +++++++--- tests/SchemaDump.hs | 4 ++-- 10 files changed, 71 insertions(+), 18 deletions(-) diff --git a/cabal.project b/cabal.project index de7ae67bb1..f0b9607dbb 100644 --- a/cabal.project +++ b/cabal.project @@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 26d149d17c0ceb5cc17d0fd1c1357d95bd47e549 + tag: e4b47825b56122222e5bf4716285b419acdac83d source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 622500603c..42647833c9 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."26d149d17c0ceb5cc17d0fd1c1357d95bd47e549" = "135knaxsyag3mlml62w2j4y8shvi82q8frhcn5b28qd8hlg5q2rq"; + "https://github.com/simplex-chat/simplexmq.git"."e4b47825b56122222e5bf4716285b419acdac83d" = "1dvr1s4kicf8z3x0bl7v6q1hphdngwcmcbmmqmj99b8728zh8fk4"; "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; "https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 48146abdc8..ee7a6b3b93 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -133,7 +133,7 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, de firstTime <- not <$> doesFileExist f currentUser <- newTVarIO user servers <- resolveServers defaultServers - smpAgent <- getSMPAgentClient aCfg {dbFile = dbFilePrefix <> "_agent.db", dbKey} servers {netCfg = networkConfig} + smpAgent <- getSMPAgentClient aCfg {dbFile = agentStoreFile dbFilePrefix, dbKey} servers {netCfg = networkConfig} agentAsync <- newTVarIO Nothing idsDrg <- newTVarIO =<< drgNew inputQ <- newTBQueueIO tbqSize diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index 99097d7e07..13b77b2023 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -23,7 +23,7 @@ simplexChatCore cfg@ChatConfig {yesToMigrations} opts sendToast chat where initRun = do let f = chatStoreFile $ dbFilePrefix opts - st <- createStore f (dbKey opts) yesToMigrations + st <- createChatStore f (dbKey opts) yesToMigrations u <- getCreateActiveUser st cc <- newChatController st (Just u) cfg opts sendToast runSimplexChat opts u cc chat diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 03cbf055e1..2017b2703e 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -1,18 +1,23 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} module Simplex.Chat.Mobile where import Control.Concurrent.STM +import Control.Exception (catch) import Control.Monad.Reader import Data.Aeson (ToJSON (..)) import qualified Data.Aeson as J import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB +import Data.Functor (($>)) import Data.List (find) import Data.Maybe (fromMaybe) +import Database.SQLite.Simple (SQLError (..)) +import qualified Database.SQLite.Simple as DB import Foreign.C.String import Foreign.C.Types (CInt (..)) import Foreign.StablePtr @@ -24,11 +29,17 @@ import Simplex.Chat.Options import Simplex.Chat.Store import Simplex.Chat.Types import Simplex.Chat.Util (safeDecodeUtf8) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (yesToMigrations)) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (yesToMigrations), createAgentStore) +import Simplex.Messaging.Agent.Store.SQLite (closeSQLiteStore) import Simplex.Messaging.Client (defaultNetworkConfig) +import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (CorrId (..)) +import Simplex.Messaging.Util (catchAll) import System.Timeout (timeout) +foreign export ccall "chat_migrate_db" cChatMigrateDB :: CString -> CString -> IO CJSONString + +-- chat_init is deprecated foreign export ccall "chat_init" cChatInit :: CString -> IO (StablePtr ChatController) foreign export ccall "chat_init_key" cChatInitKey :: CString -> CString -> IO (StablePtr ChatController) @@ -41,7 +52,13 @@ foreign export ccall "chat_recv_msg_wait" cChatRecvMsgWait :: StablePtr ChatCont foreign export ccall "chat_parse_markdown" cChatParseMarkdown :: CString -> IO CJSONString --- | initialize chat controller +-- | check and migrate the database +-- This function validates that the encryption is correct and runs migrations - it should be called before cChatInitKey +cChatMigrateDB :: CString -> CString -> IO CJSONString +cChatMigrateDB fp key = + ((,) <$> peekCAString fp <*> peekCAString key) >>= uncurry chatMigrateDB >>= newCAString . LB.unpack . J.encode + +-- | initialize chat controller (deprecated) -- The active user has to be created and the chat has to be started before most commands can be used. cChatInit :: CString -> IO (StablePtr ChatController) cChatInit fp = peekCAString fp >>= chatInit >>= newStablePtr @@ -99,13 +116,39 @@ type CJSONString = CString getActiveUser_ :: SQLiteStore -> IO (Maybe User) getActiveUser_ st = find activeUser <$> withTransaction st getUsers +data DBMigrationResult + = DBMOk + | DBMErrorNotADatabase {dbFile :: String} + | DBMError {dbFile :: String, migrationError :: String} + deriving (Show, Generic) + +instance ToJSON DBMigrationResult where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "DBM" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "DBM" + +chatMigrateDB :: String -> String -> IO DBMigrationResult +chatMigrateDB dbFilePrefix dbKey = + migrate createChatStore (chatStoreFile dbFilePrefix) >>= \case + DBMOk -> migrate createAgentStore (agentStoreFile dbFilePrefix) + e -> pure e + where + migrate createStore dbFile = + ((createStore dbFile dbKey True >>= closeSQLiteStore) $> DBMOk) + `catch` (pure . checkDBError) + `catchAll` (pure . dbError) + where + checkDBError e = case sqlError e of + DB.ErrorNotADatabase -> DBMErrorNotADatabase dbFile + _ -> dbError e + dbError e = DBMError dbFile $ show e + chatInit :: String -> IO ChatController chatInit = (`chatInitKey` "") chatInitKey :: String -> String -> IO ChatController chatInitKey dbFilePrefix dbKey = do let f = chatStoreFile dbFilePrefix - chatStore <- createStore f dbKey (yesToMigrations (defaultMobileConfig :: ChatConfig)) + chatStore <- createChatStore f dbKey (yesToMigrations (defaultMobileConfig :: ChatConfig)) user_ <- getActiveUser_ chatStore newChatController chatStore user_ defaultMobileConfig mobileChatOpts {dbFilePrefix} Nothing diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 56ca84a1b2..7b9b96e3f8 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -20,8 +20,9 @@ module Simplex.Chat.Store ( SQLiteStore, StoreError (..), - createStore, + createChatStore, chatStoreFile, + agentStoreFile, createUser, getUsers, setActiveUser, @@ -276,12 +277,15 @@ migrations = sortBy (compare `on` name) $ map migration schemaMigrations where migration (name, query) = Migration {name = name, up = fromQuery query} -createStore :: FilePath -> String -> Bool -> IO SQLiteStore -createStore dbFilePath dbKey = createSQLiteStore dbFilePath dbKey migrations +createChatStore :: FilePath -> String -> Bool -> IO SQLiteStore +createChatStore dbFilePath dbKey = createSQLiteStore dbFilePath dbKey migrations chatStoreFile :: FilePath -> FilePath chatStoreFile = (<> "_chat.db") +agentStoreFile :: FilePath -> FilePath +agentStoreFile = (<> "_agent.db") + checkConstraint :: StoreError -> ExceptT StoreError IO a -> ExceptT StoreError IO a checkConstraint err action = ExceptT $ runExceptT action `E.catch` (pure . Left . handleSQLError err) diff --git a/stack.yaml b/stack.yaml index 0e4cde28be..9cf489bbb5 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: 26d149d17c0ceb5cc17d0fd1c1357d95bd47e549 + commit: e4b47825b56122222e5bf4716285b419acdac83d # - ../direct-sqlcipher - github: simplex-chat/direct-sqlcipher commit: 34309410eb2069b029b8fc1872deb1e0db123294 diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 40e12b1835..00197bc58b 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -105,14 +105,14 @@ testCfgV1 = testCfg {agentConfig = testAgentCfgV1} createTestChat :: ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC createTestChat cfg opts@ChatOpts {dbKey} dbPrefix profile = do let dbFilePrefix = testDBPrefix <> dbPrefix - st <- createStore (dbFilePrefix <> "_chat.db") dbKey False + st <- createChatStore (dbFilePrefix <> "_chat.db") dbKey False Right user <- withTransaction st $ \db -> runExceptT $ createUser db profile True startTestChat_ st cfg opts dbFilePrefix user startTestChat :: ChatConfig -> ChatOpts -> String -> IO TestCC startTestChat cfg opts@ChatOpts {dbKey} dbPrefix = do let dbFilePrefix = testDBPrefix <> dbPrefix - st <- createStore (dbFilePrefix <> "_chat.db") dbKey False + st <- createChatStore (dbFilePrefix <> "_chat.db") dbKey False Just user <- find activeUser <$> withTransaction st getUsers startTestChat_ st cfg opts dbFilePrefix user diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index fd66923a65..a7d4aaa70a 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -73,7 +73,9 @@ parsedMarkdown = "{\"formattedText\":[{\"format\":{\"type\":\"bold\"},\"text\":\ testChatApiNoUser :: IO () testChatApiNoUser = withTmpFiles $ do + DBMOk <- chatMigrateDB testDBPrefix "" cc <- chatInit testDBPrefix + DBMErrorNotADatabase _ <- chatMigrateDB testDBPrefix "myKey" chatSendCmd cc "/u" `shouldReturn` noActiveUser chatSendCmd cc "/_start" `shouldReturn` noActiveUser chatSendCmd cc "/u alice Alice" `shouldReturn` activeUser @@ -81,10 +83,14 @@ testChatApiNoUser = withTmpFiles $ do testChatApi :: IO () testChatApi = withTmpFiles $ do - let f = chatStoreFile $ testDBPrefix <> "1" - st <- createStore f "" True + let dbPrefix = testDBPrefix <> "1" + f = chatStoreFile dbPrefix + st <- createChatStore f "myKey" True Right _ <- withTransaction st $ \db -> runExceptT $ createUser db aliceProfile True - cc <- chatInit $ testDBPrefix <> "1" + DBMOk <- chatMigrateDB testDBPrefix "myKey" + cc <- chatInitKey dbPrefix "myKey" + DBMErrorNotADatabase _ <- chatMigrateDB testDBPrefix "" + DBMErrorNotADatabase _ <- chatMigrateDB testDBPrefix "anotherKey" chatSendCmd cc "/u" `shouldReturn` activeUser chatSendCmd cc "/u alice Alice" `shouldReturn` activeUserExists chatSendCmd cc "/_start" `shouldReturn` chatStarted diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index 7140846fbb..808e1773a4 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -5,7 +5,7 @@ module SchemaDump where import ChatClient (withTmpFiles) import Control.DeepSeq import Control.Monad (void) -import Simplex.Chat.Store (createStore) +import Simplex.Chat.Store (createChatStore) import System.Process (readCreateProcess, shell) import Test.Hspec @@ -22,7 +22,7 @@ schemaDumpTest = testVerifySchemaDump :: IO () testVerifySchemaDump = withTmpFiles $ do - void $ createStore testDB "" False + void $ createChatStore testDB "" False void $ readCreateProcess (shell $ "touch " <> schema) "" savedSchema <- readFile schema savedSchema `deepseq` pure () From 7999e88554eddb99ab20d40c44e5f9605af4734a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 2 Sep 2022 20:20:43 +0100 Subject: [PATCH 07/44] core: fix chatInitKey to pass database key to agent store (#1010) --- src/Simplex/Chat/Mobile.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 2017b2703e..0e3d68d7a2 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -150,7 +150,7 @@ chatInitKey dbFilePrefix dbKey = do let f = chatStoreFile dbFilePrefix chatStore <- createChatStore f dbKey (yesToMigrations (defaultMobileConfig :: ChatConfig)) user_ <- getActiveUser_ chatStore - newChatController chatStore user_ defaultMobileConfig mobileChatOpts {dbFilePrefix} Nothing + newChatController chatStore user_ defaultMobileConfig mobileChatOpts {dbFilePrefix, dbKey} Nothing chatSendCmd :: ChatController -> String -> IO JSONString chatSendCmd cc s = LB.unpack . J.encode . APIResponse Nothing <$> runReaderT (execChatCommand $ B.pack s) cc From ddde821064f0a7db1cd879e0ae9de37505b85012 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 2 Sep 2022 22:03:53 +0100 Subject: [PATCH 08/44] nix: direct-sqlcipher patch, openssl flag for android (#1011) --- flake.nix | 23 +++++++++++++++++++---- scripts/nix/direct-sqlcipher-2.3.27.patch | 12 ++++++++++++ scripts/nix/direct-sqlite-2.3.26.patch | 15 --------------- 3 files changed, 31 insertions(+), 19 deletions(-) create mode 100644 scripts/nix/direct-sqlcipher-2.3.27.patch delete mode 100644 scripts/nix/direct-sqlite-2.3.26.patch diff --git a/flake.nix b/flake.nix index 23f22ee737..819c1db22c 100644 --- a/flake.nix +++ b/flake.nix @@ -25,7 +25,7 @@ }; sha256map = import ./scripts/nix/sha256map.nix; modules = [{ - packages.direct-sqlite.patches = [ ./scripts/nix/direct-sqlite-2.3.26.patch ]; + packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-2.3.27.patch ]; packages.entropy.patches = [ ./scripts/nix/entropy.patch ]; } ({ pkgs,lib, ... }: lib.mkIf (pkgs.stdenv.hostPlatform.isAndroid) { @@ -91,7 +91,12 @@ > $out/nix-support/hydra-build-products ''; }; - "aarch64-android:lib:simplex-chat" = (drv androidPkgs).simplex-chat.components.library.override { + "aarch64-android:lib:simplex-chat" = (drv' { + pkgs' = androidPkgs; + extra-modules = [{ + packages.direct-sqlcipher.flags.openssl = true; + }]; + }).simplex-chat.components.library.override { smallAddressSpace = true; enableShared = false; # for android we build a shared library, passing these arguments is a bit tricky, as # we want only the threaded rts (HSrts_thr) and ffi to be linked, but not fed into iserv for @@ -138,7 +143,12 @@ > $out/nix-support/hydra-build-products ''; }; - "x86_64-android:lib:simplex-chat" = (drv androidPkgs).simplex-chat.components.library.override { + "x86_64-android:lib:simplex-chat" = (drv' { + pkgs' = androidPkgs; + extra-modules = [{ + packages.direct-sqlcipher.flags.openssl = true; + }]; + }).simplex-chat.components.library.override { smallAddressSpace = true; enableShared = false; # for android we build a shared library, passing these arguments is a bit tricky, as # we want only the threaded rts (HSrts_thr) and ffi to be linked, but not fed into iserv for @@ -185,7 +195,12 @@ > $out/nix-support/hydra-build-products ''; }; - "x86_64-linux:lib:simplex-chat" = (drv androidPkgs).simplex-chat.components.library.override { + "x86_64-linux:lib:simplex-chat" = (drv' { + pkgs' = androidPkgs; + extra-modules = [{ + packages.direct-sqlcipher.flags.openssl = true; + }]; + }).simplex-chat.components.library.override { smallAddressSpace = true; enableShared = false; # for android we build a shared library, passing these arguments is a bit tricky, as # we want only the threaded rts (HSrts_thr) and ffi to be linked, but not fed into iserv for diff --git a/scripts/nix/direct-sqlcipher-2.3.27.patch b/scripts/nix/direct-sqlcipher-2.3.27.patch new file mode 100644 index 0000000000..3fec71357a --- /dev/null +++ b/scripts/nix/direct-sqlcipher-2.3.27.patch @@ -0,0 +1,12 @@ +diff --git a/direct-sqlcipher.cabal b/direct-sqlcipher.cabal +index 728ba3e..c63745e 100644 +--- a/direct-sqlcipher.cabal ++++ b/direct-sqlcipher.cabal +@@ -84,6 +84,8 @@ library + cc-options: -DSQLITE_TEMP_STORE=2 + -DSQLITE_HAS_CODEC + ++ extra-libraries: dl ++ + if !os(windows) && !os(android) + extra-libraries: pthread diff --git a/scripts/nix/direct-sqlite-2.3.26.patch b/scripts/nix/direct-sqlite-2.3.26.patch deleted file mode 100644 index 9ac2196ddd..0000000000 --- a/scripts/nix/direct-sqlite-2.3.26.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/direct-sqlite.cabal b/direct-sqlite.cabal -index 96f26b7..996198e 100644 ---- a/direct-sqlite.cabal -+++ b/direct-sqlite.cabal -@@ -69,7 +69,9 @@ library - install-includes: sqlite3.h, sqlite3ext.h - include-dirs: cbits - -- if !os(windows) && !os(android) -+ extra-libraries: dl -+ -+ if !os(windows) && !os(android) - extra-libraries: pthread - - if flag(fulltextsearch) From 4734758be01a0ac198457912eb695635c8010e62 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 2 Sep 2022 22:44:49 +0100 Subject: [PATCH 09/44] fix flake.nix --- flake.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index 819c1db22c..7b3ae460fb 100644 --- a/flake.nix +++ b/flake.nix @@ -91,7 +91,7 @@ > $out/nix-support/hydra-build-products ''; }; - "aarch64-android:lib:simplex-chat" = (drv' { + "aarch64-android:lib:simplex-chat" = (drv { pkgs' = androidPkgs; extra-modules = [{ packages.direct-sqlcipher.flags.openssl = true; @@ -143,7 +143,7 @@ > $out/nix-support/hydra-build-products ''; }; - "x86_64-android:lib:simplex-chat" = (drv' { + "x86_64-android:lib:simplex-chat" = (drv { pkgs' = androidPkgs; extra-modules = [{ packages.direct-sqlcipher.flags.openssl = true; @@ -195,7 +195,7 @@ > $out/nix-support/hydra-build-products ''; }; - "x86_64-linux:lib:simplex-chat" = (drv' { + "x86_64-linux:lib:simplex-chat" = (drv { pkgs' = androidPkgs; extra-modules = [{ packages.direct-sqlcipher.flags.openssl = true; From 19f3890bed165866aa1e67e295754b86f746971a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 3 Sep 2022 09:22:19 +0100 Subject: [PATCH 10/44] update flake.nix --- flake.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index 7b3ae460fb..819c1db22c 100644 --- a/flake.nix +++ b/flake.nix @@ -91,7 +91,7 @@ > $out/nix-support/hydra-build-products ''; }; - "aarch64-android:lib:simplex-chat" = (drv { + "aarch64-android:lib:simplex-chat" = (drv' { pkgs' = androidPkgs; extra-modules = [{ packages.direct-sqlcipher.flags.openssl = true; @@ -143,7 +143,7 @@ > $out/nix-support/hydra-build-products ''; }; - "x86_64-android:lib:simplex-chat" = (drv { + "x86_64-android:lib:simplex-chat" = (drv' { pkgs' = androidPkgs; extra-modules = [{ packages.direct-sqlcipher.flags.openssl = true; @@ -195,7 +195,7 @@ > $out/nix-support/hydra-build-products ''; }; - "x86_64-linux:lib:simplex-chat" = (drv { + "x86_64-linux:lib:simplex-chat" = (drv' { pkgs' = androidPkgs; extra-modules = [{ packages.direct-sqlcipher.flags.openssl = true; From a8216bbd54f435a970508f27647f4e9bba131d98 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 3 Sep 2022 19:32:21 +0100 Subject: [PATCH 11/44] core: add error strings to SQLCipher encrypt/decrypt commands (#1014) --- src/Simplex/Chat/Archive.hs | 2 +- src/Simplex/Chat/Controller.hs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Simplex/Chat/Archive.hs b/src/Simplex/Chat/Archive.hs index d67a76520a..8811e3d155 100644 --- a/src/Simplex/Chat/Archive.hs +++ b/src/Simplex/Chat/Archive.hs @@ -130,7 +130,7 @@ export f key key' = do where withDB a err = liftIO (bracket (SQL.open $ T.pack f) SQL.close a) - `catch` \(e :: SomeException) -> liftIO (putStrLn $ "Database error: " <> show e) >> throwDBError err + `catch` \(e :: SomeException) -> liftIO (putStrLn $ "Database error: " <> show e) >> throwDBError (err $ show e) exportSQL = T.unlines $ keySQL key diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 92340dfb8d..d6ab336825 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -434,8 +434,8 @@ instance ToJSON ChatErrorType where data DatabaseError = DBENotEncrypted | DBENoFile - | DBEExportFailed - | DBEOpenFailed + | DBEExportFailed {databaseError :: String} + | DBEOpenFailed {databaseError :: String} deriving (Show, Exception, Generic) instance ToJSON DatabaseError where From 082e12683be87580968ffd00115ea771ca3ad3ae Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 5 Sep 2022 14:54:39 +0100 Subject: [PATCH 12/44] core: change database encryption API to require current passphrase on all changes (#1019) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 14 ++--- src/Simplex/Chat/Archive.hs | 94 ++++++++++++++++------------------ src/Simplex/Chat/Controller.hs | 34 +++++++++--- src/Simplex/Chat/View.hs | 3 +- stack.yaml | 2 +- tests/ChatTests.hs | 8 +-- 8 files changed, 88 insertions(+), 71 deletions(-) diff --git a/cabal.project b/cabal.project index f0b9607dbb..02264df7f3 100644 --- a/cabal.project +++ b/cabal.project @@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: e4b47825b56122222e5bf4716285b419acdac83d + tag: 50c210c5c0c7f792c39123c2177bb60b307295b9 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 42647833c9..df144900e3 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."e4b47825b56122222e5bf4716285b419acdac83d" = "1dvr1s4kicf8z3x0bl7v6q1hphdngwcmcbmmqmj99b8728zh8fk4"; + "https://github.com/simplex-chat/simplexmq.git"."50c210c5c0c7f792c39123c2177bb60b307295b9" = "1f23p5crfy8fhfmcv96r7c6xpzgj2ab8nwqzdhis6mskhrfhyj4g"; "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; "https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index ee7a6b3b93..ea552836fd 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -26,7 +26,7 @@ import Data.Bifunctor (first) import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B -import Data.Char (isSpace, ord) +import Data.Char (isSpace) import Data.Either (fromRight) import Data.Fixed (div') import Data.Functor (($>)) @@ -238,8 +238,7 @@ processChatCommand = \case APIExportArchive cfg -> checkChatStopped $ exportArchive cfg $> CRCmdOk APIImportArchive cfg -> withStoreChanged $ importArchive cfg APIDeleteStorage -> withStoreChanged $ deleteStorage - APIEncryptStorage key -> checkStoreNotChanged . withStoreChanged $ encryptStorage key - APIDecryptStorage -> checkStoreNotChanged $ withStoreChanged decryptStorage + APIStorageEncryption cfg -> withStoreChanged $ sqlCipherExport cfg APIGetChats withPCC -> CRApiChats <$> withUser (\user -> withStore' $ \db -> getChatPreviews db user withPCC) APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of CTDirect -> CRApiChat . AChat SCTDirect <$> withStore (\db -> getDirectChat db user cId pagination search) @@ -2538,8 +2537,10 @@ chatCommandP = "/_db export " *> (APIExportArchive <$> jsonP), "/_db import " *> (APIImportArchive <$> jsonP), "/_db delete" $> APIDeleteStorage, - "/db encrypt " *> (APIEncryptStorage <$> encryptionKeyP), - "/db decrypt" $> APIDecryptStorage, + "/_db encryption" *> (APIStorageEncryption <$> jsonP), + "/db encrypt " *> (APIStorageEncryption . DBEncryptionConfig "" <$> dbKeyP), + "/db password " *> (APIStorageEncryption <$> (DBEncryptionConfig <$> dbKeyP <* A.space <*> dbKeyP)), + "/db decrypt " *> (APIStorageEncryption . (`DBEncryptionConfig` "") <$> dbKeyP), "/_get chats" *> (APIGetChats <$> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False)), "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP <*> optional searchP), "/_get items count=" *> (APIGetChatItems <$> A.decimal), @@ -2689,7 +2690,8 @@ chatCommandP = t_ <- optional $ " timeout=" *> A.decimal let tcpTimeout = 1000000 * fromMaybe (maybe 5 (const 10) socksProxy) t_ pure $ fullNetworkConfig socksProxy tcpTimeout - encryptionKeyP = B.unpack <$> A.takeWhile1 (\c -> ord c >= 0x20 && ord c <= 0x7E) + dbKeyP = nonEmptyKey <$?> strP + nonEmptyKey k@(DBEncryptionKey s) = if null s then Left "empty key" else Right k adminContactReq :: ConnReqContact adminContactReq = diff --git a/src/Simplex/Chat/Archive.hs b/src/Simplex/Chat/Archive.hs index 8811e3d155..d2f33d92fd 100644 --- a/src/Simplex/Chat/Archive.hs +++ b/src/Simplex/Chat/Archive.hs @@ -8,8 +8,7 @@ module Simplex.Chat.Archive ( exportArchive, importArchive, deleteStorage, - encryptStorage, - decryptStorage, + sqlCipherExport, ) where @@ -21,7 +20,7 @@ import qualified Database.SQLite3 as SQL import Simplex.Chat.Controller import Simplex.Messaging.Agent.Client (agentStore) import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), sqlString) -import Simplex.Messaging.Util (unlessM, whenM) +import Simplex.Messaging.Util (ifM, unlessM, whenM) import System.FilePath import UnliftIO.Directory import UnliftIO.Exception (SomeException, bracket, catch) @@ -87,63 +86,58 @@ deleteStorage = do data StorageFiles = StorageFiles { chatDb :: FilePath, - chatKey :: String, + chatEncrypted :: TVar Bool, agentDb :: FilePath, - agentKey :: String, + agentEncrypted :: TVar Bool, filesPath :: Maybe FilePath } storageFiles :: ChatMonad m => m StorageFiles storageFiles = do ChatController {chatStore, filesFolder, smpAgent} <- ask - let SQLiteStore {dbFilePath = chatDb, dbKey = chatKey} = chatStore - SQLiteStore {dbFilePath = agentDb, dbKey = agentKey} = agentStore smpAgent + let SQLiteStore {dbFilePath = chatDb, dbEncrypted = chatEncrypted} = chatStore + SQLiteStore {dbFilePath = agentDb, dbEncrypted = agentEncrypted} = agentStore smpAgent filesPath <- readTVarIO filesFolder - pure StorageFiles {chatDb, chatKey, agentDb, agentKey, filesPath} + pure StorageFiles {chatDb, chatEncrypted, agentDb, agentEncrypted, filesPath} -encryptStorage :: forall m. ChatMonad m => String -> m () -encryptStorage key' = updateDatabase $ \f key -> export f key key' - -decryptStorage :: forall m. ChatMonad m => m () -decryptStorage = updateDatabase $ \f -> \case - "" -> throwDBError DBENotEncrypted - key -> export f key "" - -updateDatabase :: ChatMonad m => (FilePath -> String -> m ()) -> m () -updateDatabase update = do - fs@StorageFiles {chatDb, chatKey, agentDb, agentKey} <- storageFiles - checkFile `with` fs - backup `with` fs - (update chatDb chatKey >> update agentDb agentKey) - `catchError` \e -> (restore `with` fs) >> throwError e +sqlCipherExport :: forall m. ChatMonad m => DBEncryptionConfig -> m () +sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = DBEncryptionKey key'} = + when (key /= key') $ do + fs@StorageFiles {chatDb, chatEncrypted, agentDb, agentEncrypted} <- storageFiles + checkFile `with` fs + backup `with` fs + (export chatDb chatEncrypted >> export agentDb agentEncrypted) + `catchError` \e -> (restore `with` fs) >> throwError e where action `with` StorageFiles {chatDb, agentDb} = action chatDb >> action agentDb backup f = copyFile f (f <> ".bak") restore f = copyFile (f <> ".bak") f - checkFile f = unlessM (doesFileExist f) $ throwDBError DBENoFile - -export :: ChatMonad m => FilePath -> String -> String -> m () -export f key key' = do - withDB (`SQL.exec` exportSQL) DBEExportFailed - renameFile (f <> ".exported") f - withDB (`SQL.exec` testSQL) DBEOpenFailed - where - withDB a err = - liftIO (bracket (SQL.open $ T.pack f) SQL.close a) - `catch` \(e :: SomeException) -> liftIO (putStrLn $ "Database error: " <> show e) >> throwDBError (err $ show e) - exportSQL = - T.unlines $ - keySQL key - <> [ "ATTACH DATABASE " <> sqlString (f <> ".exported") <> " AS exported KEY " <> sqlString key' <> ";", - "SELECT sqlcipher_export('exported');", - "DETACH DATABASE exported;" - ] - testSQL = - T.unlines $ - keySQL key' - <> [ "PRAGMA foreign_keys = ON;", - "PRAGMA secure_delete = ON;", - "PRAGMA auto_vacuum = FULL;", - "SELECT count(*) FROM sqlite_master;" - ] - keySQL k = ["PRAGMA key = " <> sqlString k <> ";" | not (null k)] + checkFile f = unlessM (doesFileExist f) $ throwDBError $ DBErrorNoFile f + export f dbEnc = do + enc <- readTVarIO dbEnc + when (enc && null key) $ throwDBError DBErrorEncrypted + when (not enc && not (null key)) $ throwDBError DBErrorPlaintext + withDB (`SQL.exec` exportSQL) DBErrorExport + renameFile (f <> ".exported") f + withDB (`SQL.exec` testSQL) DBErrorOpen + atomically $ writeTVar dbEnc $ not (null key') + where + withDB a err = + liftIO (bracket (SQL.open $ T.pack f) SQL.close a) + `catch` \(e :: SomeException) -> liftIO (putStrLn $ "Database error: " <> show e) >> throwDBError (err $ show e) + exportSQL = + T.unlines $ + keySQL key + <> [ "ATTACH DATABASE " <> sqlString (f <> ".exported") <> " AS exported KEY " <> sqlString key' <> ";", + "SELECT sqlcipher_export('exported');", + "DETACH DATABASE exported;" + ] + testSQL = + T.unlines $ + keySQL key' + <> [ "PRAGMA foreign_keys = ON;", + "PRAGMA secure_delete = ON;", + "PRAGMA auto_vacuum = FULL;", + "SELECT count(*) FROM sqlite_master;" + ] + keySQL k = ["PRAGMA key = " <> sqlString k <> ";" | not (null k)] diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index d6ab336825..714fdbcc7c 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -17,9 +17,13 @@ import Control.Monad.Reader import Crypto.Random (ChaChaDRG) import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as J +import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) +import qualified Data.ByteString.Char8 as B +import Data.Char (ord) import Data.Int (Int64) import Data.Map.Strict (Map) +import Data.String import Data.Text (Text) import Data.Time (ZonedTime) import Data.Time.Clock (UTCTime) @@ -38,8 +42,9 @@ import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, InitialAgentServers, Net import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore) import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) -import Simplex.Messaging.Parsers (dropPrefix, enumJSON, sumTypeJSON) +import Simplex.Messaging.Parsers (dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) import Simplex.Messaging.Protocol (AProtocolType, CorrId, MsgFlags) import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport.Client (TransportHost) @@ -112,8 +117,7 @@ data ChatCommand | APIExportArchive ArchiveConfig | APIImportArchive ArchiveConfig | APIDeleteStorage - | APIEncryptStorage String - | APIDecryptStorage + | APIStorageEncryption DBEncryptionConfig | APIGetChats {pendingConnections :: Bool} | APIGetChat ChatRef ChatPagination (Maybe String) | APIGetChatItems Int @@ -324,6 +328,21 @@ instance ToJSON ChatResponse where data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool, parentTempDirectory :: Maybe FilePath} deriving (Show, Generic, FromJSON) +data DBEncryptionConfig = DBEncryptionConfig {currentKey :: DBEncryptionKey, newKey :: DBEncryptionKey} + deriving (Show, Generic, FromJSON) + +newtype DBEncryptionKey = DBEncryptionKey String + deriving (Show) + +instance IsString DBEncryptionKey where fromString = parseString $ parseAll strP + +instance StrEncoding DBEncryptionKey where + strEncode (DBEncryptionKey s) = B.pack s + strP = DBEncryptionKey . B.unpack <$> A.takeWhile (\c -> c /= ' ' && ord c >= 0x21 && ord c <= 0x7E) + +instance FromJSON DBEncryptionKey where + parseJSON = strParseJSON "DBEncryptionKey" + data ContactSubStatus = ContactSubStatus { contact :: Contact, contactError :: Maybe ChatError @@ -432,10 +451,11 @@ instance ToJSON ChatErrorType where toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CE" data DatabaseError - = DBENotEncrypted - | DBENoFile - | DBEExportFailed {databaseError :: String} - | DBEOpenFailed {databaseError :: String} + = DBErrorEncrypted + | DBErrorPlaintext + | DBErrorNoFile {dbFile :: String} + | DBErrorExport {databaseError :: String} + | DBErrorOpen {databaseError :: String} deriving (Show, Exception, Generic) instance ToJSON DatabaseError where diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 2e9b34c908..84e1bdb939 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -933,7 +933,8 @@ viewChatError = \case SEQuotedChatItemNotFound -> ["message not found - reply is not sent"] e -> ["chat db error: " <> sShow e] ChatErrorDatabase err -> case err of - DBENotEncrypted -> ["error: chat database is not encrypted"] + DBErrorEncrypted -> ["error: chat database is already encrypted"] + DBErrorPlaintext -> ["error: chat database is not encrypted"] e -> ["chat database error: " <> sShow e] ChatErrorAgent err -> case err of SMP SMP.AUTH -> diff --git a/stack.yaml b/stack.yaml index 9cf489bbb5..ef535363c4 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: e4b47825b56122222e5bf4716285b419acdac83d + commit: 50c210c5c0c7f792c39123c2177bb60b307295b9 # - ../direct-sqlcipher - github: simplex-chat/direct-sqlcipher commit: 34309410eb2069b029b8fc1872deb1e0db123294 diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index eef6e9d8e0..bc787641f5 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -2769,11 +2769,11 @@ testDatabaseEncryption = withTmpFiles $ do bob <# "alice> hi" alice ##> "/db encrypt mykey" alice <## "error: chat not stopped" - alice ##> "/db decrypt" + alice ##> "/db decrypt mykey" alice <## "error: chat not stopped" alice ##> "/_stop" alice <## "chat stopped" - alice ##> "/db decrypt" + alice ##> "/db decrypt mykey" alice <## "error: chat database is not encrypted" alice ##> "/db encrypt mykey" alice <## "ok" @@ -2785,7 +2785,7 @@ testDatabaseEncryption = withTmpFiles $ do testChatWorking alice bob alice ##> "/_stop" alice <## "chat stopped" - alice ##> "/db encrypt nextkey" + alice ##> "/db password mykey nextkey" alice <## "ok" withTestChatOpts testOpts {maintenance = True, dbKey = "nextkey"} "alice" $ \alice -> do alice ##> "/_start" @@ -2793,7 +2793,7 @@ testDatabaseEncryption = withTmpFiles $ do testChatWorking alice bob alice ##> "/_stop" alice <## "chat stopped" - alice ##> "/db decrypt" + alice ##> "/db decrypt nextkey" alice <## "ok" withTestChat "alice" $ \alice -> testChatWorking alice bob From 9d009663baa2faee7c4cd1a37b715a1af0f29c45 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 5 Sep 2022 15:40:40 +0100 Subject: [PATCH 13/44] update flake.nix to export libs from androidPkgs.openssl --- flake.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flake.nix b/flake.nix index 819c1db22c..a8968dbbcb 100644 --- a/flake.nix +++ b/flake.nix @@ -116,6 +116,8 @@ # find ${androidPkgs.gmp6.override { withStatic = true; }}/lib -name "*.a" -exec cp {} $out/_pkg \; # find ${androidIconv}/lib -name "*.a" -exec cp {} $out/_pkg \; # find ${androidPkgs.stdenv.cc.libc}/lib -name "*.a" -exec cp {} $out/_pkg \; + echo ${androidPkgs.openssl} + find ${androidPkgs.openssl}/lib -name "*.so" -exec cp {} $out/_pkg \; ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsimplex.so From 0d220d63eaabeecbc04ecdbc6914ca04014d4003 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 5 Sep 2022 22:14:35 +0100 Subject: [PATCH 14/44] update flake.nix --- flake.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index a8968dbbcb..28d9e4d9b4 100644 --- a/flake.nix +++ b/flake.nix @@ -117,7 +117,8 @@ # find ${androidIconv}/lib -name "*.a" -exec cp {} $out/_pkg \; # find ${androidPkgs.stdenv.cc.libc}/lib -name "*.a" -exec cp {} $out/_pkg \; echo ${androidPkgs.openssl} - find ${androidPkgs.openssl}/lib -name "*.so" -exec cp {} $out/_pkg \; + ls -l ${androidPkgs.openssl} + find ${androidPkgs.openssl} -name "*.so" -exec cp {} $out/_pkg \; ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsimplex.so From 7072dd4f7e742f10fa563633854e115532d3052f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 6 Sep 2022 21:25:07 +0100 Subject: [PATCH 15/44] core: fix api for encryption (#1025) --- src/Simplex/Chat.hs | 2 +- tests/ChatTests.hs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 43584287aa..70d0272c55 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -2544,7 +2544,7 @@ chatCommandP = "/_db export " *> (APIExportArchive <$> jsonP), "/_db import " *> (APIImportArchive <$> jsonP), "/_db delete" $> APIDeleteStorage, - "/_db encryption" *> (APIStorageEncryption <$> jsonP), + "/_db encryption " *> (APIStorageEncryption <$> jsonP), "/db encrypt " *> (APIStorageEncryption . DBEncryptionConfig "" <$> dbKeyP), "/db password " *> (APIStorageEncryption <$> (DBEncryptionConfig <$> dbKeyP <* A.space <*> dbKeyP)), "/db decrypt " *> (APIStorageEncryption . (`DBEncryptionConfig` "") <$> dbKeyP), diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 54b01bcfb5..6a6ea4ac74 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -2790,13 +2790,15 @@ testDatabaseEncryption = withTmpFiles $ do alice <## "chat stopped" alice ##> "/db password mykey nextkey" alice <## "ok" - withTestChatOpts testOpts {maintenance = True, dbKey = "nextkey"} "alice" $ \alice -> do + alice ##> "/_db encryption {\"currentKey\":\"nextkey\",\"newKey\":\"anotherkey\"}" + alice <## "ok" + withTestChatOpts testOpts {maintenance = True, dbKey = "anotherkey"} "alice" $ \alice -> do alice ##> "/_start" alice <## "chat started" testChatWorking alice bob alice ##> "/_stop" alice <## "chat stopped" - alice ##> "/db decrypt nextkey" + alice ##> "/db decrypt anotherkey" alice <## "ok" withTestChat "alice" $ \alice -> testChatWorking alice bob From de2d169fbc17939cedef3552bb8c67927dca58b6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 6 Sep 2022 23:14:58 +0100 Subject: [PATCH 16/44] core: report passphrase error separately from others (#1027) --- src/Simplex/Chat/Archive.hs | 10 +++++++++- src/Simplex/Chat/Controller.hs | 17 ++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/Simplex/Chat/Archive.hs b/src/Simplex/Chat/Archive.hs index 6eb23bcc0d..86c2176f67 100644 --- a/src/Simplex/Chat/Archive.hs +++ b/src/Simplex/Chat/Archive.hs @@ -124,7 +124,15 @@ sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = D where withDB a err = liftIO (bracket (SQL.open $ T.pack f) SQL.close a) - `catch` \(e :: SomeException) -> liftIO (putStrLn $ "Database error: " <> show e) >> throwDBError (err $ show e) + `catch` (\(e :: SQL.SQLError) -> log' e >> checkSQLError e) + `catch` (\(e :: SomeException) -> log' e >> throwSQLError e) + where + log' e = liftIO . putStrLn $ "Database error: " <> show e + checkSQLError e = case SQL.sqlError e of + SQL.ErrorNotADatabase -> throwDBError $ err SQLiteErrorNotADatabase + _ -> throwSQLError e + throwSQLError :: Show e => e -> m () + throwSQLError = throwDBError . err . SQLiteError . show exportSQL = T.unlines $ keySQL key diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 37527ea065..9d4d1cae4b 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -393,7 +393,7 @@ data ChatError = ChatError {errorType :: ChatErrorType} | ChatErrorAgent {agentError :: AgentErrorType} | ChatErrorStore {storeError :: StoreError} - | ChatErrorDatabase {database :: DatabaseError} + | ChatErrorDatabase {databaseError :: DatabaseError} deriving (Show, Exception, Generic) instance ToJSON ChatError where @@ -455,13 +455,20 @@ data DatabaseError = DBErrorEncrypted | DBErrorPlaintext | DBErrorNoFile {dbFile :: String} - | DBErrorExport {databaseError :: String} - | DBErrorOpen {databaseError :: String} + | DBErrorExport {sqliteError :: SQLiteError} + | DBErrorOpen {sqliteError :: SQLiteError} deriving (Show, Exception, Generic) instance ToJSON DatabaseError where - toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "DBE" - toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "DBE" + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "DB" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "DB" + +data SQLiteError = SQLiteErrorNotADatabase | SQLiteError String + deriving (Show, Exception, Generic) + +instance ToJSON SQLiteError where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "SQLite" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "SQLite" throwDBError :: ChatMonad m => DatabaseError -> m () throwDBError = throwError . ChatErrorDatabase From 00471a095dc425d288b30624805a0d0b7684277a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 7 Sep 2022 10:24:42 +0100 Subject: [PATCH 17/44] update flake.nix to export openssl libs --- flake.nix | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index 28d9e4d9b4..f47f53636f 100644 --- a/flake.nix +++ b/flake.nix @@ -116,9 +116,7 @@ # find ${androidPkgs.gmp6.override { withStatic = true; }}/lib -name "*.a" -exec cp {} $out/_pkg \; # find ${androidIconv}/lib -name "*.a" -exec cp {} $out/_pkg \; # find ${androidPkgs.stdenv.cc.libc}/lib -name "*.a" -exec cp {} $out/_pkg \; - echo ${androidPkgs.openssl} - ls -l ${androidPkgs.openssl} - find ${androidPkgs.openssl} -name "*.so" -exec cp {} $out/_pkg \; + find ${androidPkgs.openssl.out}/lib -name "*.so" -exec cp {} $out/_pkg \; ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsimplex.so From 766009269e8809713fab8a973386642b254dc15c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 7 Sep 2022 12:49:41 +0100 Subject: [PATCH 18/44] ios: use SQLCipher (#1012) * use sqlcipher build, hardcoded encryption key * UI for db encryption * database passphrase UI * show orange icon when database is not encrypted * call encrypt * more ios ux * basic UX for passphrase complete * with animation Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> * passphrase complexity, fixes * fix moving entry field Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> --- apps/ios/Shared/ContentView.swift | 3 + apps/ios/Shared/Model/ChatModel.swift | 2 + apps/ios/Shared/Model/SimpleXAPI.swift | 35 +- apps/ios/Shared/Model/SuspendChat.swift | 4 +- apps/ios/Shared/SimpleXApp.swift | 2 +- .../Database/DatabaseEncryptionView.swift | 343 ++++++++++++++++++ .../Views/Database/DatabaseErrorView.swift | 93 +++++ .../Shared/Views/Database/DatabaseView.swift | 42 ++- .../Views/UserSettings/SettingsView.swift | 7 +- apps/ios/SimpleX (iOS).entitlements | 4 + apps/ios/SimpleX NSE/SimpleX NSE.entitlements | 4 + apps/ios/SimpleX.xcodeproj/project.pbxproj | 52 ++- apps/ios/SimpleXChat/API.swift | 59 ++- apps/ios/SimpleXChat/APITypes.swift | 39 ++ apps/ios/SimpleXChat/AppGroup.swift | 10 +- apps/ios/SimpleXChat/FileUtils.swift | 10 +- apps/ios/SimpleXChat/KeyChain.swift | 107 ++++++ apps/ios/SimpleXChat/SimpleX.h | 3 +- 18 files changed, 756 insertions(+), 63 deletions(-) create mode 100644 apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift create mode 100644 apps/ios/Shared/Views/Database/DatabaseErrorView.swift create mode 100644 apps/ios/SimpleXChat/KeyChain.swift diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 272a327e71..9c2ee6827b 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import SimpleXChat struct ContentView: View { @EnvironmentObject var chatModel: ChatModel @@ -21,6 +22,8 @@ struct ContentView: View { ZStack { if prefPerformLA && userAuthorized != true { Button(action: runAuthenticate) { Label("Unlock", systemImage: "lock") } + } else if let status = chatModel.chatDbStatus, status != .ok { + DatabaseErrorView(status: status) } else if !chatModel.v3DBMigration.startChat { MigrateToAppGroupView() } else if let step = chatModel.onboardingStage { diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index ab1739846a..d349294a0f 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -18,6 +18,8 @@ final class ChatModel: ObservableObject { @Published var currentUser: User? @Published var chatRunning: Bool? @Published var chatDbChanged = false + @Published var chatDbEncrypted: Bool? + @Published var chatDbStatus: DBMigrationResult? // list of chat "previews" @Published var chats: [Chat] = [] // current chat diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 6db4b01075..f5a866b2b4 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -94,7 +94,7 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)") } DispatchQueue.main.async { - ChatModel.shared.terminalItems.append(.cmd(.now, cmd)) + ChatModel.shared.terminalItems.append(.cmd(.now, cmd.obfuscated)) ChatModel.shared.terminalItems.append(.resp(.now, resp)) } return resp @@ -185,6 +185,10 @@ func apiDeleteStorage() async throws { try await sendCommandOkResp(.apiDeleteStorage) } +func apiStorageEncryption(currentKey: String = "", newKey: String = "") async throws { + try await sendCommandOkResp(.apiStorageEncryption(config: DBEncryptionConfig(currentKey: currentKey, newKey: newKey))) +} + func apiGetChats() throws -> [ChatData] { let r = chatSendCmdSync(.apiGetChats) if case let .apiChats(chats) = r { return chats } @@ -654,22 +658,21 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws throw r } -func initializeChat(start: Bool) throws { +func initializeChat(start: Bool, dbKey: String? = nil) throws { logger.debug("initializeChat") - do { - let m = ChatModel.shared - try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) - try apiSetIncognito(incognito: incognitoGroupDefault.get()) - m.currentUser = try apiGetActiveUser() - if m.currentUser == nil { - m.onboardingStage = .step1_SimpleXInfo - } else if start { - try startChat() - } else { - m.chatRunning = false - } - } catch { - fatalError("Failed to initialize chat controller or database: \(responseError(error))") + let m = ChatModel.shared + (m.chatDbEncrypted, m.chatDbStatus) = migrateChatDatabase(dbKey) + if m.chatDbStatus != .ok { return } + let _ = getChatCtrl(dbKey) + try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) + try apiSetIncognito(incognito: incognitoGroupDefault.get()) + m.currentUser = try apiGetActiveUser() + if m.currentUser == nil { + m.onboardingStage = .step1_SimpleXInfo + } else if start { + try startChat() + } else { + m.chatRunning = false } } diff --git a/apps/ios/Shared/Model/SuspendChat.swift b/apps/ios/Shared/Model/SuspendChat.swift index 2c0261ae8f..33ad5af8ed 100644 --- a/apps/ios/Shared/Model/SuspendChat.swift +++ b/apps/ios/Shared/Model/SuspendChat.swift @@ -71,9 +71,9 @@ private func _chatSuspended() { } } -func activateChat(appState: AppState = .active) { +func activateChat(appState: AppState = .active, databaseReady: Bool = true) { suspendLockQueue.sync { appStateGroupDefault.set(appState) - apiActivateChat() + if databaseReady { apiActivateChat() } } } diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 386ae0c431..6061e7ce52 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -64,7 +64,7 @@ struct SimpleXApp: App { ChatReceiver.shared.start() } let appState = appStateGroupDefault.get() - activateChat() + activateChat(databaseReady: chatModel.chatDbStatus == .ok) if appState.inactive && chatModel.chatRunning == true { updateChats() updateCallInvitations() diff --git a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift new file mode 100644 index 0000000000..374133355b --- /dev/null +++ b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift @@ -0,0 +1,343 @@ +// +// DatabaseEncryptionView.swift +// SimpleX (iOS) +// +// Created by Evgeny on 04/09/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +enum DatabaseEncryptionAlert: Identifiable { + case keychainRemoveKey + case encryptDatabaseSaved + case encryptDatabase + case changeDatabaseKeySaved + case changeDatabaseKey + case databaseEncrypted + case currentPassphraseError + case error(title: LocalizedStringKey, error: String = "") + + var id: String { + switch self { + case .keychainRemoveKey: return "keychainRemoveKey" + case .encryptDatabaseSaved: return "encryptDatabaseSaved" + case .encryptDatabase: return "encryptDatabase" + case .changeDatabaseKeySaved: return "changeDatabaseKeySaved" + case .changeDatabaseKey: return "changeDatabaseKey" + case .databaseEncrypted: return "databaseEncrypted" + case .currentPassphraseError: return "currentPassphraseError" + case let .error(title, _): return "error \(title)" + } + } +} + +struct DatabaseEncryptionView: View { + @EnvironmentObject private var m: ChatModel + @State private var alert: DatabaseEncryptionAlert? = nil + @State private var progressIndicator = false + @State private var useKeychain = storeDBPassphraseGroupDefault.get() + @State private var useKeychainToggle = storeDBPassphraseGroupDefault.get() + @State private var initialRandomDBPassphrase = initialRandomDBPassphraseGroupDefault.get() + @State private var storedKey = getDatabaseKey() != nil + @State private var currentKey = "" + @State private var newKey = "" + @State private var confirmNewKey = "" + @State private var currentKeyShown = false + + var body: some View { + ZStack { + databaseEncryptionView() + if progressIndicator { + ProgressView().scaleEffect(2) + } + } + } + + private func databaseEncryptionView() -> some View { + List { + Section { + settingsRow("key") { + Toggle("Save passphrase in Keychain", isOn: $useKeychainToggle) + .onChange(of: useKeychainToggle) { _ in + if useKeychainToggle { + setUseKeychain(true) + } else if storedKey { + alert = .keychainRemoveKey + } else { + setUseKeychain(false) + } + } + .disabled(initialRandomDBPassphrase) + } + + if !initialRandomDBPassphrase && m.chatDbEncrypted == true { + DatabaseKeyField(key: $currentKey, placeholder: "Current passphrase…", valid: validKey(currentKey)) + } + + DatabaseKeyField(key: $newKey, placeholder: "New passphrase…", valid: validKey(newKey), showStrength: true) + DatabaseKeyField(key: $confirmNewKey, placeholder: "Confirm new passphrase…", valid: confirmNewKey == "" || newKey == confirmNewKey) + + settingsRow("lock.rotation") { + Button("Update database passphrase") { + alert = currentKey == "" + ? (useKeychain ? .encryptDatabaseSaved : .encryptDatabase) + : (useKeychain ? .changeDatabaseKeySaved : .changeDatabaseKey) + } + } + .disabled( + currentKey == newKey || + newKey != confirmNewKey || + newKey == "" || + !validKey(currentKey) || + !validKey(newKey) + ) + } header: { + Text("") + } footer: { + VStack(alignment: .leading, spacing: 16) { + if m.chatDbEncrypted == false { + Text("Your chat database is not encrypted - set passphrase to encrypt it.") + } else if useKeychain { + if storedKey { + Text("iOS Keychain is used to securely store passphrase - it allows receiving push notifications.") + if initialRandomDBPassphrase { + Text("Database is encrypted using a random passphrase, you can change it.") + } else { + Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.") + } + } else { + Text("iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.") + } + } else { + Text("You have to enter passphrase every time the app starts - it is not stored on the device.") + Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.") + if m.notificationMode == .instant && m.notificationPreview != .hidden { + Text("**Warning**: Instant push notifications require passphrase saved in Keychain.") + } + } + } + .padding(.top, 1) + .font(.callout) + } + } + .onAppear { + if initialRandomDBPassphrase { currentKey = getDatabaseKey() ?? "" } + } + .disabled(m.chatRunning != false) + .alert(item: $alert) { item in databaseEncryptionAlert(item) } + } + + private func encryptDatabase() { + progressIndicator = true + Task { + do { + try await apiStorageEncryption(currentKey: currentKey, newKey: newKey) + initialRandomDBPassphraseGroupDefault.set(false) + if useKeychain { + if setDatabaseKey(newKey) { + await resetFormAfterEncryption(true) + await operationEnded(.databaseEncrypted) + } else { + await resetFormAfterEncryption() + await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain")) + } + } else { + await resetFormAfterEncryption() + await operationEnded(.databaseEncrypted) + } + } catch let error { + if case .chatCmdError(.errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse { + await operationEnded(.currentPassphraseError) + } else { + await operationEnded(.error(title: "Error encrypting database", error: responseError(error))) + } + } + } + } + + private func resetFormAfterEncryption(_ stored: Bool = false) async { + await MainActor.run { + m.chatDbEncrypted = true + initialRandomDBPassphrase = false + currentKey = "" + newKey = "" + confirmNewKey = "" + storedKey = stored + } + } + + private func setUseKeychain(_ value: Bool) { + useKeychain = value + storeDBPassphraseGroupDefault.set(value) + } + + private func databaseEncryptionAlert(_ alertItem: DatabaseEncryptionAlert) -> Alert { + switch alertItem { + case .keychainRemoveKey: + return Alert( + title: Text("Remove passphrase from keychain?"), + message: Text("Instant push notifications will be hidden!\n") + storeSecurelyDanger(), + primaryButton: .destructive(Text("Remove")) { + if removeDatabaseKey() { + setUseKeychain(false) + storedKey = false + } else { + alert = .error(title: "Keychain error", error: "Failed to remove passphrase") + } + }, + secondaryButton: .cancel() { + withAnimation { useKeychainToggle = true } + } + ) + case .encryptDatabaseSaved: + return Alert( + title: Text("Encrypt database?"), + message: Text("Database will be encrypted and the passphrase stored in the keychain.\n") + storeSecurelySaved(), + primaryButton: .default(Text("Encrypt")) { encryptDatabase() }, + secondaryButton: .cancel() + ) + case .encryptDatabase: + return Alert( + title: Text("Encrypt database?"), + message: Text("Database will be encrypted.\n") + storeSecurelyDanger(), + primaryButton: .destructive(Text("Encrypt")) { encryptDatabase() }, + secondaryButton: .cancel() + ) + case .changeDatabaseKeySaved: + return Alert( + title: Text("Change database passphrase?"), + message: Text("Database encryption passphrase will be updated and stored in the keychain.\n") + storeSecurelySaved(), + primaryButton: .default(Text("Update")) { encryptDatabase() }, + secondaryButton: .cancel() + ) + case .changeDatabaseKey: + return Alert( + title: Text("Change database passphrase?"), + message: Text("Database encryption passphrase will be updated.\n") + storeSecurelyDanger(), + primaryButton: .destructive(Text("Update")) { encryptDatabase() }, + secondaryButton: .cancel() + ) + case .databaseEncrypted: + return Alert(title: Text("Database encrypted!")) + case .currentPassphraseError: + return Alert( + title: Text("Wrong passsphrase!"), + message: Text("Please enter correct current passphrase") + ) + case let .error(title, error): + return Alert(title: Text(title), message: Text("\(error)")) + } + } + + private func storeSecurelySaved() -> Text { + Text("Please store passphrase securely, you will NOT be able to change it if you lose it.") + } + + private func storeSecurelyDanger() -> Text { + Text("Please store passphrase securely, you will NOT be able to access chat if you lose it.") + } + + private func operationEnded(_ dbAlert: DatabaseEncryptionAlert) async { + await MainActor.run { + m.chatDbChanged = true + progressIndicator = false + alert = dbAlert + } + } +} + + +struct DatabaseKeyField: View { + @Binding var key: String + var placeholder: LocalizedStringKey + var valid: Bool + var showStrength = false + @State private var showKey = false + + var body: some View { + ZStack(alignment: .leading) { + let iconColor = valid + ? (showStrength && key != "" ? PassphraseStrength(passphrase: key).color : .secondary) + : .red + Image(systemName: valid ? (showKey ? "eye.slash" : "eye") : "exclamationmark.circle") + .resizable() + .scaledToFit() + .frame(width: 20, height: 22, alignment: .center) + .foregroundColor(iconColor) + .onTapGesture { showKey = !showKey } + textField() + .disableAutocorrection(true) + .autocapitalization(.none) + .submitLabel(.done) + .padding(.leading, 36) + } + } + + @ViewBuilder func textField() -> some View { + if showKey { + TextField(placeholder, text: $key) + } else { + SecureField(placeholder, text: $key) + } + } +} + +// based on https://generatepasswords.org/how-to-calculate-entropy/ +private func passphraseEnthropy(_ s: String) -> Double { + var hasDigits = false + var hasUppercase = false + var hasLowercase = false + var hasSymbols = false + for c in s { + if c.isNumber { + hasDigits = true + } else if c.isLetter { + if c.isUppercase { hasUppercase = true } + else { hasLowercase = true } + } else if c.isASCII { + hasSymbols = true + } + } + let poolSize: Double = (hasDigits ? 10 : 0) + (hasUppercase ? 26 : 0) + (hasLowercase ? 26 : 0) + (hasSymbols ? 32 : 0) + return Double(s.count) * log2(poolSize) +} + +enum PassphraseStrength { + case veryWeak + case weak + case reasonable + case strong + + init(passphrase s: String) { + let enthropy = passphraseEnthropy(s) + self = enthropy > 60 + ? .strong + : enthropy > 45 + ? .reasonable + : enthropy > 30 + ? .weak + : .veryWeak + } + + var color: Color { + switch self { + case .veryWeak: return .red + case .weak: return .orange + case .reasonable: return .yellow + case .strong: return .green + } + } +} + +func validKey(_ s: String) -> Bool { + for c in s { if c.isWhitespace || !c.isASCII { return false } } + return true +} + +struct DatabaseEncryptionView_Previews: PreviewProvider { + static var previews: some View { + DatabaseEncryptionView() + } +} diff --git a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift new file mode 100644 index 0000000000..310c0d96ea --- /dev/null +++ b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift @@ -0,0 +1,93 @@ +// +// DatabaseErrorView.swift +// SimpleX (iOS) +// +// Created by Evgeny on 04/09/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct DatabaseErrorView: View { + @EnvironmentObject var m: ChatModel + var status: DBMigrationResult + @State private var dbKey = "" + @State private var storedDBKey = getDatabaseKey() + @State private var useKeychain = storeDBPassphraseGroupDefault.get() + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + switch status { + case let .errorNotADatabase(dbFile): + if useKeychain && storedDBKey != nil && storedDBKey != "" { + Text("Wrong database passphrase").font(.title) + Text("Database passphrase is different from saved in the keychain.") + DatabaseKeyField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey)) + saveAndOpenButton() + Spacer() + Text("File: \(dbFile)") + } else { + Text("Encrypted database").font(.title) + Text("Database passphrase is required to open chat.") + DatabaseKeyField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey)) + if useKeychain { + saveAndOpenButton() + } else { + openChatButton() + } + Spacer() + } + case let .error(dbFile, migrationError): + Text("Database error") + .font(.title) + Text("File: \(dbFile)") + Text("Error: \(migrationError)") + Spacer() + case .errorKeychain: + Text("Keychain error") + .font(.title) + Text("Cannot access keychain to save database password") + Spacer() + case let .unknown(json): + Text("Database error") + .font(.title) + Text("Unknown database error: \(json)") + Spacer() + case .ok: + EmptyView() + } + } + .padding() + .frame(maxHeight: .infinity) } + + private func saveAndOpenButton() -> some View { + Button("Save passphrase and open chat") { + if setDatabaseKey(dbKey) { + storeDBPassphraseGroupDefault.set(true) + initialRandomDBPassphraseGroupDefault.set(false) + } + do { + try initializeChat(start: m.v3DBMigration.startChat, dbKey: dbKey) + } catch let error { + logger.error("initializeChat \(responseError(error))") + } + } + } + + private func openChatButton() -> some View { + Button("Open chat") { + do { + try initializeChat(start: m.v3DBMigration.startChat, dbKey: dbKey) + } catch let error { + logger.error("initializeChat \(responseError(error))") + } + } + } +} + +struct DatabaseErrorView_Previews: PreviewProvider { + static var previews: some View { + DatabaseErrorView(status: .errorNotADatabase(dbFile: "simplex_v1_chat.db")) + } +} diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index d904cd05ca..8f7ba7cca4 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -11,6 +11,7 @@ import SimpleXChat enum DatabaseAlert: Identifiable { case stopChat + case exportProhibited case importArchive case archiveImported case deleteChat @@ -21,6 +22,7 @@ enum DatabaseAlert: Identifiable { var id: String { switch self { case .stopChat: return "stopChat" + case .exportProhibited: return "exportProhibited" case .importArchive: return "importArchive" case .archiveImported: return "archiveImported" case .deleteChat: return "deleteChat" @@ -82,18 +84,28 @@ struct DatabaseView: View { } Section { - settingsRow("square.and.arrow.up") { - Button { - exportArchive() + let unencrypted = m.chatDbEncrypted == false + let color: Color = unencrypted ? .orange : .secondary + settingsRow(unencrypted ? "lock.open" : "lock", color: color) { + NavigationLink { + DatabaseEncryptionView() + .navigationTitle("Database passphrase") } label: { - Text("Export database") + Text("Database passphrase") + } + } + settingsRow("square.and.arrow.up") { + Button("Export database") { + if initialRandomDBPassphraseGroupDefault.get() { + alert = .exportProhibited + } else { + exportArchive() + } } } settingsRow("square.and.arrow.down") { - Button(role: .destructive) { + Button("Import database", role: .destructive) { showFileImporter = true - } label: { - Text("Import database") } } if let archiveName = chatArchiveName { @@ -110,10 +122,8 @@ struct DatabaseView: View { } } settingsRow("trash.slash") { - Button(role: .destructive) { + Button("Delete database", role: .destructive) { alert = .deleteChat - } label: { - Text("Delete database") } } } header: { @@ -130,10 +140,8 @@ struct DatabaseView: View { if case .group = dbContainer, legacyDatabase { Section("Old database") { settingsRow("trash") { - Button { + Button("Delete old database") { alert = .deleteLegacyDatabase - } label: { - Text("Delete old database") } } } @@ -166,6 +174,11 @@ struct DatabaseView: View { withAnimation { runChat = true } } ) + case .exportProhibited: + return Alert( + title: Text("Set passphrase to export"), + message: Text("Database is encrypted using a random passphrase. Please change it before exporting.") + ) case .importArchive: if let fileURL = importedArchivePath { return Alert( @@ -254,6 +267,7 @@ struct DatabaseView: View { do { let config = ArchiveConfig(archivePath: archivePath.path) try await apiImportArchive(config: config) + _ = removeDatabaseKey() await operationEnded(.archiveImported) } catch let error { await operationEnded(.error(title: "Error importing chat database", error: responseError(error))) @@ -273,6 +287,8 @@ struct DatabaseView: View { Task { do { try await apiDeleteStorage() + _ = removeDatabaseKey() + storeDBPassphraseGroupDefault.set(true) await operationEnded(.chatDeleted) } catch let error { await operationEnded(.error(title: "Error deleting database", error: responseError(error))) diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 3a2fe30ae5..2fff1adc92 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -44,7 +44,7 @@ let appDefaults: [String: Any] = [ DEFAULT_ACCENT_COLOR_RED: 0.000, DEFAULT_ACCENT_COLOR_GREEN: 0.533, DEFAULT_ACCENT_COLOR_BLUE: 1.000, - DEFAULT_USER_INTERFACE_STYLE: 0 + DEFAULT_USER_INTERFACE_STYLE: 0, ] private var indent: CGFloat = 36 @@ -92,9 +92,10 @@ struct SettingsView: View { DatabaseView(showSettings: $showSettings) .navigationTitle("Your chat database") } label: { - settingsRow("internaldrive") { + let color: Color = chatModel.chatDbEncrypted == false ? .orange : .secondary + settingsRow("internaldrive", color: color) { HStack { - Text("Database export & import") + Text("Database passphrase & export") Spacer() if chatModel.chatRunning == false { Image(systemName: "exclamationmark.octagon.fill").foregroundColor(.red) diff --git a/apps/ios/SimpleX (iOS).entitlements b/apps/ios/SimpleX (iOS).entitlements index 6dda31ceba..51672d6290 100644 --- a/apps/ios/SimpleX (iOS).entitlements +++ b/apps/ios/SimpleX (iOS).entitlements @@ -14,5 +14,9 @@ group.chat.simplex.app + keychain-access-groups + + $(AppIdentifierPrefix)chat.simplex.app + diff --git a/apps/ios/SimpleX NSE/SimpleX NSE.entitlements b/apps/ios/SimpleX NSE/SimpleX NSE.entitlements index 82cf32be67..51dea2c806 100644 --- a/apps/ios/SimpleX NSE/SimpleX NSE.entitlements +++ b/apps/ios/SimpleX NSE/SimpleX NSE.entitlements @@ -6,5 +6,9 @@ group.chat.simplex.app + keychain-access-groups + + $(AppIdentifierPrefix)chat.simplex.app + diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 060d05d5f9..46cfbb2b8c 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -13,11 +13,12 @@ 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; }; 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; }; 5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00164328A26FBC0094D739 /* ContextMenu.swift */; }; - 5C00166A28C119300094D739 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166528C119300094D739 /* libgmp.a */; }; - 5C00166B28C119300094D739 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166628C119300094D739 /* libffi.a */; }; - 5C00166C28C119300094D739 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166728C119300094D739 /* libgmpxx.a */; }; - 5C00166D28C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166828C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a */; }; - 5C00166E28C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166928C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a */; }; + 5C00167528C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00166F28C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a */; }; + 5C00167728C28A6B0094D739 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00167028C28A6B0094D739 /* libgmpxx.a */; }; + 5C00167928C28A6B0094D739 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00167128C28A6B0094D739 /* libgmp.a */; }; + 5C00167B28C28A6B0094D739 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00167228C28A6B0094D739 /* libffi.a */; }; + 5C00167D28C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C00167328C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a */; }; + 5C00168128C4FE760094D739 /* KeyChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00168028C4FE760094D739 /* KeyChain.swift */; }; 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; }; 5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; }; 5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; }; @@ -61,6 +62,8 @@ 5C9C2DA52894777E00CC63B1 /* GroupProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9C2DA42894777E00CC63B1 /* GroupProfileView.swift */; }; 5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */; }; 5C9C2DA92899DA6F00CC63B1 /* NetworkAndServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */; }; + 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 */; }; 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; }; 5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; }; @@ -197,11 +200,12 @@ 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = ""; }; 3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = ""; }; 5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = ""; }; - 5C00166528C119300094D739 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5C00166628C119300094D739 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5C00166728C119300094D739 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5C00166828C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a"; sourceTree = ""; }; - 5C00166928C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a"; sourceTree = ""; }; + 5C00166F28C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a"; sourceTree = ""; }; + 5C00167028C28A6B0094D739 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5C00167128C28A6B0094D739 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5C00167228C28A6B0094D739 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5C00167328C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a"; sourceTree = ""; }; + 5C00168028C4FE760094D739 /* KeyChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyChain.swift; sourceTree = ""; }; 5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = ""; }; 5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = ""; }; 5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = ""; }; @@ -247,6 +251,8 @@ 5C9C2DA42894777E00CC63B1 /* GroupProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupProfileView.swift; sourceTree = ""; }; 5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedNetworkSettings.swift; sourceTree = ""; }; 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 = ""; }; 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; 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 = ""; }; @@ -351,13 +357,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5C00166E28C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a in Frameworks */, + 5C00167728C28A6B0094D739 /* libgmpxx.a in Frameworks */, + 5C00167B28C28A6B0094D739 /* libffi.a in Frameworks */, + 5C00167D28C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a in Frameworks */, + 5C00167928C28A6B0094D739 /* libgmp.a in Frameworks */, + 5C00167528C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5C00166C28C119300094D739 /* libgmpxx.a in Frameworks */, - 5C00166A28C119300094D739 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5C00166B28C119300094D739 /* libffi.a in Frameworks */, - 5C00166D28C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -412,11 +418,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5C00166628C119300094D739 /* libffi.a */, - 5C00166528C119300094D739 /* libgmp.a */, - 5C00166728C119300094D739 /* libgmpxx.a */, - 5C00166828C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme-ghc8.10.7.a */, - 5C00166928C119300094D739 /* libHSsimplex-chat-3.2.1-DtA3whUOI1LFNbOU0tXQme.a */, + 5C00167228C28A6B0094D739 /* libffi.a */, + 5C00167128C28A6B0094D739 /* libgmp.a */, + 5C00167028C28A6B0094D739 /* libgmpxx.a */, + 5C00167328C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG-ghc8.10.7.a */, + 5C00166F28C28A6B0094D739 /* libHSsimplex-chat-3.2.1-Iu3WidfJ6Vb3MmatyiWdoG.a */, ); path = Libraries; sourceTree = ""; @@ -596,6 +602,7 @@ 5CDCAD7D2818941F00503DA2 /* API.swift */, 5CDCAD80281A7E2700503DA2 /* Notifications.swift */, 64DAE1502809D9F5000DA960 /* FileUtils.swift */, + 5C00168028C4FE760094D739 /* KeyChain.swift */, 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */, 5CE2BA8A2845332200EC33A6 /* SimpleX.h */, 5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */, @@ -642,6 +649,8 @@ 5C4B3B09285FB130003915F2 /* DatabaseView.swift */, 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */, 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */, + 5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */, + 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */, ); path = Database; sourceTree = ""; @@ -858,6 +867,7 @@ 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */, 5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */, 5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */, + 5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */, 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */, 5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */, 5C029EAA283942EA004A9677 /* CallController.swift in Sources */, @@ -934,6 +944,7 @@ 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */, 5CE4407227ADB1D0007B033A /* Emoji.swift in Sources */, 5C3F1D5A2844B4DE00EC8A82 /* ExperimentalFeaturesView.swift in Sources */, + 5C9CC7A928C532AB00BEF955 /* DatabaseErrorView.swift in Sources */, 5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */, 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */, 5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */, @@ -962,6 +973,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5C00168128C4FE760094D739 /* KeyChain.swift in Sources */, 5CE2BA97284537A800EC33A6 /* dummy.m in Sources */, 5CE2BA922845340900EC33A6 /* FileUtils.swift in Sources */, 5CE2BA91284533A300EC33A6 /* Notifications.swift in Sources */, diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index f718305d86..45362a0de7 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -10,16 +10,46 @@ import Foundation private var chatController: chat_ctrl? -public func getChatCtrl() -> chat_ctrl { +public func getChatCtrl(_ useKey: String? = nil) -> chat_ctrl { if let controller = chatController { return controller } let dbPath = getAppDatabasePath().path + let dbKey = useKey ?? getDatabaseKey() ?? "" logger.debug("getChatCtrl DB path: \(dbPath)") - var cstr = dbPath.cString(using: .utf8)! - chatController = chat_init(&cstr) - logger.debug("getChatCtrl: chat_init") + var cPath = dbPath.cString(using: .utf8)! + var cKey = dbKey.cString(using: .utf8)! + chatController = chat_init_key(&cPath, &cKey) + logger.debug("getChatCtrl: chat_init_key") return chatController! } +public func migrateChatDatabase(_ useKey: String? = nil) -> (Bool, DBMigrationResult) { + logger.debug("migrateChatDatabase \(storeDBPassphraseGroupDefault.get())") + let dbPath = getAppDatabasePath().path + var dbKey = "" + let useKeychain = storeDBPassphraseGroupDefault.get() + if let key = useKey { + dbKey = key + } else if useKeychain { + if !hasDatabase() { + dbKey = randomDatabasePassword() + initialRandomDBPassphraseGroupDefault.set(true) + } else if let key = getDatabaseKey() { + dbKey = key + } + } + logger.debug("migrateChatDatabase DB path: \(dbPath)") +// logger.debug("migrateChatDatabase DB key: \(dbKey)") + var cPath = dbPath.cString(using: .utf8)! + var cKey = dbKey.cString(using: .utf8)! + let cjson = chat_migrate_db(&cPath, &cKey)! + let res = dbMigrationResult(fromCString(cjson)) + let encrypted = dbKey != "" + if case .ok = res, useKeychain && encrypted && !setDatabaseKey(dbKey) { + return (encrypted, .errorKeychain) + } + return (encrypted, res) +} + public func resetChatCtrl() { chatController = nil } @@ -103,3 +133,24 @@ public func responseError(_ err: Error) -> String { return err.localizedDescription } } + +public enum DBMigrationResult: Decodable, Equatable { + case ok + case errorNotADatabase(dbFile: String) + case error(dbFile: String, migrationError: String) + case errorKeychain + case unknown(json: String) +} + +func dbMigrationResult(_ s: String) -> DBMigrationResult { + let d = s.data(using: .utf8)! +// TODO is there a way to do it without copying the data? e.g: +// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson)) +// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free) + do { + return try jsonDecoder.decode(DBMigrationResult.self, from: d) + } catch let error { + logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)") + return .unknown(json: s) + } +} diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 2299bd899a..6d548e86f3 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -24,6 +24,7 @@ public enum ChatCommand { case apiExportArchive(config: ArchiveConfig) case apiImportArchive(config: ArchiveConfig) case apiDeleteStorage + case apiStorageEncryption(config: DBEncryptionConfig) case apiGetChats case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String) case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent) @@ -88,6 +89,7 @@ public enum ChatCommand { case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))" case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" case .apiDeleteStorage: return "/_db delete" + case let .apiStorageEncryption(cfg): return "/_db encryption \(encodeJSON(cfg))" case .apiGetChats: return "/_get chats pcc=on" case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)") @@ -156,6 +158,7 @@ public enum ChatCommand { case .apiExportArchive: return "apiExportArchive" case .apiImportArchive: return "apiImportArchive" case .apiDeleteStorage: return "apiDeleteStorage" + case .apiStorageEncryption: return "apiStorageEncryption" case .apiGetChats: return "apiGetChats" case .apiGetChat: return "apiGetChat" case .apiSendMessage: return "apiSendMessage" @@ -214,6 +217,18 @@ public enum ChatCommand { func smpServersStr(smpServers: [String]) -> String { smpServers.isEmpty ? "default" : smpServers.joined(separator: ",") } + + public var obfuscated: ChatCommand { + switch self { + case let .apiStorageEncryption(cfg): + return .apiStorageEncryption(config: DBEncryptionConfig(currentKey: obfuscate(cfg.currentKey), newKey: obfuscate(cfg.newKey))) + default: return self + } + } + + private func obfuscate(_ s: String) -> String { + s == "" ? "" : "***" + } } struct APIResponse: Decodable { @@ -527,6 +542,16 @@ public struct ArchiveConfig: Encodable { } } +public struct DBEncryptionConfig: Encodable { + public init(currentKey: String, newKey: String) { + self.currentKey = currentKey + self.newKey = newKey + } + + public var currentKey: String + public var newKey: String +} + public struct NetCfg: Codable, Equatable { public var socksProxy: String? = nil public var hostMode: HostMode = .publicHost @@ -710,6 +735,7 @@ public enum ChatError: Decodable { case error(errorType: ChatErrorType) case errorAgent(agentError: AgentErrorType) case errorStore(storeError: StoreError) + case errorDatabase(databaseError: DatabaseError) } public enum ChatErrorType: Decodable { @@ -779,6 +805,19 @@ public enum StoreError: Decodable { case chatItemNotFoundByFileId(fileId: Int64) } +public enum DatabaseError: Decodable { + case errorEncrypted + case errorPlaintext + case errorNoFile(dbFile: String) + case errorExport(sqliteError: SQLiteError) + case errorOpen(sqliteError: SQLiteError) +} + +public enum SQLiteError: Decodable { + case errorNotADatabase + case error(String) +} + public enum AgentErrorType: Decodable { case CMD(cmdErr: CommandErrorType) case CONN(connErr: ConnectionErrorType) diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index d428c00832..c44ed18af9 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -24,6 +24,8 @@ let GROUP_DEFAULT_NETWORK_TCP_KEEP_IDLE = "networkTCPKeepIdle" let GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL = "networkTCPKeepIntvl" let GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT = "networkTCPKeepCnt" let GROUP_DEFAULT_INCOGNITO = "incognito" +let GROUP_DEFAULT_STORE_DB_PASSPHRASE = "storeDBPassphrase" +let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase" let APP_GROUP_NAME = "group.chat.simplex.app" @@ -39,7 +41,9 @@ public func registerGroupDefaults() { GROUP_DEFAULT_NETWORK_TCP_KEEP_IDLE: KeepAliveOpts.defaults.keepIdle, GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL: KeepAliveOpts.defaults.keepIntvl, GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT: KeepAliveOpts.defaults.keepCnt, - GROUP_DEFAULT_INCOGNITO: false + GROUP_DEFAULT_INCOGNITO: false, + GROUP_DEFAULT_STORE_DB_PASSPHRASE: true, + GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE: false ]) } @@ -96,6 +100,10 @@ public let networkUseOnionHostsGroupDefault = EnumDefault( withDefault: .no ) +public let storeDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_STORE_DB_PASSPHRASE) + +public let initialRandomDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE) + public class DateDefault { var defaults: UserDefaults var key: String diff --git a/apps/ios/SimpleXChat/FileUtils.swift b/apps/ios/SimpleXChat/FileUtils.swift index b236712910..326741d121 100644 --- a/apps/ios/SimpleXChat/FileUtils.swift +++ b/apps/ios/SimpleXChat/FileUtils.swift @@ -42,11 +42,17 @@ public func getAppDatabasePath() -> URL { dbContainerGroupDefault.get() == .group ? getGroupContainerDirectory().appendingPathComponent(DB_FILE_PREFIX, isDirectory: false) : getLegacyDatabasePath() -// getLegacyDatabasePath() } public func hasLegacyDatabase() -> Bool { - let dbPath = getLegacyDatabasePath() + hasDatabaseAtPath(getLegacyDatabasePath()) +} + +public func hasDatabase() -> Bool { + hasDatabaseAtPath(getAppDatabasePath()) +} + +func hasDatabaseAtPath(_ dbPath: URL) -> Bool { let fm = FileManager.default return fm.isReadableFile(atPath: dbPath.path + "_agent.db") && fm.isReadableFile(atPath: dbPath.path + "_chat.db") diff --git a/apps/ios/SimpleXChat/KeyChain.swift b/apps/ios/SimpleXChat/KeyChain.swift new file mode 100644 index 0000000000..704c5f752c --- /dev/null +++ b/apps/ios/SimpleXChat/KeyChain.swift @@ -0,0 +1,107 @@ +// +// KeyChain.swift +// SimpleXChat +// +// Created by Evgeny on 04/09/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import Foundation +import Security + +private let ACCESS_POLICY: CFString = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly +private let ACCESS_GROUP: String = "5NN7GUYB6T.chat.simplex.app" +private let DATABASE_PASSWORD_ITEM: String = "databasePassword" + +public func getDatabaseKey() -> String? { + getItemString(forKey: DATABASE_PASSWORD_ITEM) +} + +public func setDatabaseKey(_ key: String) -> Bool { + setItemString(key, forKey: DATABASE_PASSWORD_ITEM) +} + +public func removeDatabaseKey() -> Bool { + deleteItem(forKey: DATABASE_PASSWORD_ITEM) +} + +func randomDatabasePassword() -> String { + var keyData = Data(count: 32) + let status = keyData.withUnsafeMutableBytes { + SecRandomCopyBytes(kSecRandomDefault, 32, $0.baseAddress!) + } + if status == errSecSuccess { + return keyData.base64EncodedString() + } else { + logger.error("randomDatabasePassword: error \(status)") + return "" + } +} + +private func getItemData(forKey key: String) -> Data? { + var query = baseItemQuery(forKey: key) + query[kSecMatchLimit] = kSecMatchLimitOne + query[kSecReturnData] = true as AnyObject? + + var dataRef: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &dataRef) + if status != errSecSuccess && status != errSecItemNotFound { + logger.error("getItemData: error getting data for key '\(key)', error: \(status)") + } + return dataRef as? Data +} + +private func getItemString(forKey key: String) -> String? { + if let data = getItemData(forKey: key) { + return NSString(data: data, encoding: String.Encoding.utf8.rawValue) as? String + } + return nil +} + +private func setItemData(_ data: Data, forKey key: String) -> Bool { + var query = baseItemQuery(forKey: key) + var update = [NSString : AnyObject]() + update[kSecValueData] = data as AnyObject? + update[kSecAttrAccessible] = ACCESS_POLICY + var status: OSStatus + if getItemData(forKey: key) == nil { + for (key, value) in update { query[key] = value } + status = SecItemAdd(query as CFDictionary, nil) + } else { + status = SecItemUpdate(query as CFDictionary, update as CFDictionary) + } + if status != errSecSuccess { + logger.error("setItemData: error setting data for key '\(key)', error: \(status)") + return false + } + return true +} + +private func setItemString(_ s: String, forKey key: String) -> Bool { + if let data = s.data(using: .utf8) { + return setItemData(data, forKey: key) + } + return false +} + +private func deleteItem(forKey key: String) -> Bool { + let query = baseItemQuery(forKey: key) + if getItemData(forKey: key) != nil { + let status = SecItemDelete(query as CFDictionary) + if status != errSecSuccess { + logger.error("deleteItem: error deleting data for key '\(key)', error: \(status)") + return false + } + } + return true +} + +private func baseItemQuery(forKey key: String) -> [NSString : AnyObject] { + var query = [NSString : AnyObject]() + query[kSecClass] = kSecClassGenericPassword + query[kSecAttrAccount] = key as AnyObject? + #if TARGET_OS_IOS && !TARGET_OS_SIMULATOR + query[kSecAttrAccessGroup] = ACCESS_GROUP + #endif + return query +} diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index e848ad5cdd..1b2f5d821b 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -15,7 +15,8 @@ extern void hs_init(int argc, char **argv[]); typedef void* chat_ctrl; -extern chat_ctrl chat_init(char *path); +extern char *chat_migrate_db(char *path, char *key); +extern chat_ctrl chat_init_key(char *path, char *key); extern char *chat_send_cmd(chat_ctrl ctl, char *cmd); extern char *chat_recv_msg(chat_ctrl ctl); extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait); From 6904ad68d981430c7778540c5bd96a06754ceac1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 7 Sep 2022 12:58:01 +0100 Subject: [PATCH 19/44] android: add libcrypto.so --- apps/android/app/src/main/cpp/CMakeLists.txt | 5 ++++- scripts/android/prepare.sh | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/android/app/src/main/cpp/CMakeLists.txt b/apps/android/app/src/main/cpp/CMakeLists.txt index 28e2a25b13..e97f01708f 100644 --- a/apps/android/app/src/main/cpp/CMakeLists.txt +++ b/apps/android/app/src/main/cpp/CMakeLists.txt @@ -53,6 +53,9 @@ add_library( support SHARED IMPORTED ) set_target_properties( support PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libsupport.so) +add_library( crypto SHARED IMPORTED ) +set_target_properties( crypto PROPERTIES IMPORTED_LOCATION + ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libcrypto.so) # Specifies libraries CMake should link to your target library. You # can link multiple libraries, such as libraries you define in this @@ -61,7 +64,7 @@ set_target_properties( support PROPERTIES IMPORTED_LOCATION target_link_libraries( # Specifies the target library. app-lib - simplex support + simplex support crypto # Links the target library to the log library # included in the NDK. diff --git a/scripts/android/prepare.sh b/scripts/android/prepare.sh index 363d7f7edb..a7ee5f7e8a 100755 --- a/scripts/android/prepare.sh +++ b/scripts/android/prepare.sh @@ -2,5 +2,6 @@ # libsimplex.so and libsupport.so binaries should be in ~/Downloads folder rm ./apps/android/app/src/main/cpp/libs/arm64-v8a/* -cp ~/Downloads/libsimplex.so ./apps/android/app/src/main/cpp/libs/arm64-v8a/ cp ~/Downloads/libsupport.so ./apps/android/app/src/main/cpp/libs/arm64-v8a/ +cp ~/Downloads/pkg-aarch64-android-libsimplex/libsimplex.so ./apps/android/app/src/main/cpp/libs/arm64-v8a/ +cp ~/Downloads/pkg-aarch64-android-libsimplex/libcrypto.so ./apps/android/app/src/main/cpp/libs/arm64-v8a/ From 0fc3453f2027a8a4d40189024a20d82fa4714bb1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 7 Sep 2022 13:20:28 +0100 Subject: [PATCH 20/44] extend JNI bridge --- apps/android/app/src/main/cpp/simplex-api.c | 36 +++++++++++++++---- .../main/java/chat/simplex/app/SimplexApp.kt | 4 ++- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/apps/android/app/src/main/cpp/simplex-api.c b/apps/android/app/src/main/cpp/simplex-api.c index 5be1d1fadd..29676bd57e 100644 --- a/apps/android/app/src/main/cpp/simplex-api.c +++ b/apps/android/app/src/main/cpp/simplex-api.c @@ -24,20 +24,42 @@ Java_chat_simplex_app_SimplexAppKt_initHS(__unused JNIEnv *env, __unused jclass // from simplex-chat typedef void* chat_ctrl; -extern chat_ctrl chat_init(const char *path); +extern char *chat_migrate_db(const char *path, const char *key); +extern chat_ctrl chat_init_key(const char *path, const char *key); +extern chat_ctrl chat_init(const char *path); // deprecated extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); -extern char *chat_recv_msg(chat_ctrl ctrl); +extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); -JNIEXPORT jlong JNICALL -Java_chat_simplex_app_SimplexAppKt_chatInit(JNIEnv *env, __unused jclass clazz, jstring datadir) { - const char *_data = (*env)->GetStringUTFChars(env, datadir, JNI_FALSE); - jlong res = (jlong)chat_init(_data); - (*env)->ReleaseStringUTFChars(env, datadir, _data); +JNIEXPORT jstring JNICALL +Java_chat_simplex_app_SimplexAppKt_chatMigrateDB(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey) { + const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE); + const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE); + jstring res = (jlong)chat_migrate_db(_dbPath, _dbKey); + (*env)->ReleaseStringUTFChars(env, dbPath, _dbPath); + (*env)->ReleaseStringUTFChars(env, dbKey, _dbKey); return res; } +JNIEXPORT jlong JNICALL +Java_chat_simplex_app_SimplexAppKt_chatInitKey(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey) { + const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE); + const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE); + jlong ctrl = (jlong)chat_init_key(_dbPath, _dbKey); + (*env)->ReleaseStringUTFChars(env, dbPath, _dbPath); + (*env)->ReleaseStringUTFChars(env, dbKey, _dbKey); + return ctrl; +} + +JNIEXPORT jlong JNICALL +Java_chat_simplex_app_SimplexAppKt_chatInit(JNIEnv *env, __unused jclass clazz, jstring dbPath) { + const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE); + jlong ctrl = (jlong)chat_init(_dbPath); + (*env)->ReleaseStringUTFChars(env, dbPath, _dbPath); + return ctrl; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_app_SimplexAppKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) { const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE); diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index c2c777c80b..3f0a5bc312 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -26,7 +26,9 @@ external fun pipeStdOutToSocket(socketName: String) : Int // SimpleX API typealias ChatCtrl = Long -external fun chatInit(path: String): ChatCtrl +external fun chatMigrateDB(dbPath: String, dbKey: String): String +external fun chatInitKey(dbPath: String, dbKey: String): ChatCtrl +external fun chatInit(dbPath: String): ChatCtrl external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String external fun chatRecvMsg(ctrl: ChatCtrl): String external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String From 3f5ca84c84d64f78bdd470491fdbbc105c9ce32c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 7 Sep 2022 17:20:47 +0100 Subject: [PATCH 21/44] core: fix error reporting of sqlcipher export errors (#1029) --- src/Simplex/Chat/Archive.hs | 17 +++++++++-------- src/Simplex/Chat/View.hs | 5 +++++ tests/ChatTests.hs | 2 ++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Simplex/Chat/Archive.hs b/src/Simplex/Chat/Archive.hs index 86c2176f67..f4bcc5ca46 100644 --- a/src/Simplex/Chat/Archive.hs +++ b/src/Simplex/Chat/Archive.hs @@ -15,6 +15,7 @@ where import qualified Codec.Archive.Zip as Z import Control.Monad.Except import Control.Monad.Reader +import Data.Functor (($>)) import qualified Data.Text as T import qualified Database.SQLite3 as SQL import Simplex.Chat.Controller @@ -123,16 +124,16 @@ sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = D atomically $ writeTVar dbEnc $ not (null key') where withDB a err = - liftIO (bracket (SQL.open $ T.pack f) SQL.close a) - `catch` (\(e :: SQL.SQLError) -> log' e >> checkSQLError e) - `catch` (\(e :: SomeException) -> log' e >> throwSQLError e) + liftIO (bracket (SQL.open $ T.pack f) SQL.close a $> Nothing) + `catch` checkSQLError + `catch` (\(e :: SomeException) -> sqliteError' e) + >>= mapM_ (throwDBError . err) where - log' e = liftIO . putStrLn $ "Database error: " <> show e checkSQLError e = case SQL.sqlError e of - SQL.ErrorNotADatabase -> throwDBError $ err SQLiteErrorNotADatabase - _ -> throwSQLError e - throwSQLError :: Show e => e -> m () - throwSQLError = throwDBError . err . SQLiteError . show + SQL.ErrorNotADatabase -> pure $ Just SQLiteErrorNotADatabase + _ -> sqliteError' e + sqliteError' :: Show e => e -> m (Maybe SQLiteError) + sqliteError' = pure . Just . SQLiteError . show exportSQL = T.unlines $ keySQL key diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 7fa7d0ac34..fbf9878c65 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -946,6 +946,8 @@ viewChatError = \case ChatErrorDatabase err -> case err of DBErrorEncrypted -> ["error: chat database is already encrypted"] DBErrorPlaintext -> ["error: chat database is not encrypted"] + DBErrorExport e -> ["error encrypting database: " <> sqliteError' e] + DBErrorOpen e -> ["error opening database after encryption: " <> sqliteError' e] e -> ["chat database error: " <> sShow e] ChatErrorAgent err -> case err of SMP SMP.AUTH -> @@ -958,6 +960,9 @@ viewChatError = \case e -> ["smp agent error: " <> sShow e] where fileNotFound fileId = ["file " <> sShow fileId <> " not found"] + sqliteError' = \case + SQLiteErrorNotADatabase -> "wrong passphrase or invalid database file" + SQLiteError e -> sShow e ttyContact :: ContactName -> StyledString ttyContact = styled $ colored Green diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 6a6ea4ac74..983258fd44 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -2788,6 +2788,8 @@ testDatabaseEncryption = withTmpFiles $ do testChatWorking alice bob alice ##> "/_stop" alice <## "chat stopped" + alice ##> "/db password wrongkey nextkey" + alice <## "error encrypting database: wrong passphrase or invalid database file" alice ##> "/db password mykey nextkey" alice <## "ok" alice ##> "/_db encryption {\"currentKey\":\"nextkey\",\"newKey\":\"anotherkey\"}" From 05417fd1e871f07eff0cf4cfb4d7e386fa07f7d1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 7 Sep 2022 17:23:24 +0100 Subject: [PATCH 22/44] nix: ls androidPkgs --- flake.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flake.nix b/flake.nix index f47f53636f..a56a295d3b 100644 --- a/flake.nix +++ b/flake.nix @@ -116,6 +116,8 @@ # find ${androidPkgs.gmp6.override { withStatic = true; }}/lib -name "*.a" -exec cp {} $out/_pkg \; # find ${androidIconv}/lib -name "*.a" -exec cp {} $out/_pkg \; # find ${androidPkgs.stdenv.cc.libc}/lib -name "*.a" -exec cp {} $out/_pkg \; + echo "androidPkgs" + ls -lha ${androidPkgs} find ${androidPkgs.openssl.out}/lib -name "*.so" -exec cp {} $out/_pkg \; ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsimplex.so From 80976112078d5040d2689817ad574eb91fcd7779 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 7 Sep 2022 20:06:16 +0100 Subject: [PATCH 23/44] ios: NSE without passphrase in keychain (#1030) --- apps/ios/Shared/Model/ChatModel.swift | 2 ++ apps/ios/Shared/Model/SuspendChat.swift | 18 ++++++++++------- apps/ios/Shared/SimpleXApp.swift | 2 +- .../ios/SimpleX NSE/NotificationService.swift | 20 +++++++++++++------ apps/ios/SimpleXChat/Notifications.swift | 20 +++++++++++++++++++ 5 files changed, 48 insertions(+), 14 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index d349294a0f..19ba23766f 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -53,6 +53,8 @@ final class ChatModel: ObservableObject { static let shared = ChatModel() + static var ok: Bool { ChatModel.shared.chatDbStatus == .ok } + func hasChat(_ id: String) -> Bool { chats.first(where: { $0.id == id }) != nil } diff --git a/apps/ios/Shared/Model/SuspendChat.swift b/apps/ios/Shared/Model/SuspendChat.swift index 33ad5af8ed..499dbbb1f7 100644 --- a/apps/ios/Shared/Model/SuspendChat.swift +++ b/apps/ios/Shared/Model/SuspendChat.swift @@ -19,10 +19,14 @@ let bgSuspendTimeout: Int = 5 // seconds let terminationTimeout: Int = 3 // seconds private func _suspendChat(timeout: Int) { - appStateGroupDefault.set(.suspending) - apiSuspendChat(timeoutMicroseconds: timeout * 1000000) - let endTask = beginBGTask(chatSuspended) - DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout) + 1, execute: endTask) + if ChatModel.ok { + appStateGroupDefault.set(.suspending) + apiSuspendChat(timeoutMicroseconds: timeout * 1000000) + let endTask = beginBGTask(chatSuspended) + DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout) + 1, execute: endTask) + } else { + appStateGroupDefault.set(.suspended) + } } func suspendChat() { @@ -47,7 +51,7 @@ func terminateChat() { case .suspending: // suspend instantly if already suspending _chatSuspended() - apiSuspendChat(timeoutMicroseconds: 0) + if ChatModel.ok { apiSuspendChat(timeoutMicroseconds: 0) } case .stopped: () default: _suspendChat(timeout: terminationTimeout) @@ -71,9 +75,9 @@ private func _chatSuspended() { } } -func activateChat(appState: AppState = .active, databaseReady: Bool = true) { +func activateChat(appState: AppState = .active) { suspendLockQueue.sync { appStateGroupDefault.set(appState) - if databaseReady { apiActivateChat() } + if ChatModel.ok { apiActivateChat() } } } diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 6061e7ce52..386ae0c431 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -64,7 +64,7 @@ struct SimpleXApp: App { ChatReceiver.shared.start() } let appState = appStateGroupDefault.get() - activateChat(databaseReady: chatModel.chatDbStatus == .ok) + activateChat() if appState.inactive && chatModel.chatRunning == true { updateChats() updateCallInvitations() diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 969645e45f..0e8ebd852c 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -105,9 +105,9 @@ class NotificationService: UNNotificationServiceExtension { if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any], let nonce = ntfData["nonce"] as? String, let encNtfInfo = ntfData["message"] as? String, - let _ = startChat() { - logger.debug("NotificationService: receiveNtfMessages: chat is started") - if let ntfMsgInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { + let dbStatus = startChat() { + if case .ok = dbStatus, + let ntfMsgInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfMsgInfo), privacy: .public)") if let connEntity = ntfMsgInfo.connEntity { setBestAttemptNtf(createConnectionEventNtf(connEntity)) @@ -118,9 +118,11 @@ class NotificationService: UNNotificationServiceExtension { await PendingNtfs.shared.readStream(id, for: self, msgCount: ntfMsgInfo.ntfMessages.count) deliverBestAttemptNtf() } + return } } - return + } else { + setBestAttemptNtf(createErrorNtf(dbStatus)) } } deliverBestAttemptNtf() @@ -151,20 +153,26 @@ class NotificationService: UNNotificationServiceExtension { } } -func startChat() -> User? { +var chatStarted = false + +func startChat() -> DBMigrationResult? { hs_init(0, nil) + if chatStarted { return .ok } + let (_, dbStatus) = migrateChatDatabase() + if dbStatus != .ok { return dbStatus } if let user = apiGetActiveUser() { logger.debug("active user \(String(describing: user))") do { try setNetworkConfig(getNetCfg()) let justStarted = try apiStartChat() + chatStarted = true if justStarted { try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) try apiSetIncognito(incognito: incognitoGroupDefault.get()) chatLastStartGroupDefault.set(Date.now) Task { await receiveMessages() } } - return user + return .ok } catch { logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)") } diff --git a/apps/ios/SimpleXChat/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift index 2a4a1f6b75..6432b36506 100644 --- a/apps/ios/SimpleXChat/Notifications.swift +++ b/apps/ios/SimpleXChat/Notifications.swift @@ -119,6 +119,26 @@ public func createConnectionEventNtf(_ connEntity: ConnectionEntity) -> UNMutabl ) } +public func createErrorNtf(_ dbStatus: DBMigrationResult) -> UNMutableNotificationContent { + var title: String + switch dbStatus { + case .errorNotADatabase: + title = NSLocalizedString("Encrypted message: no passphrase", comment: "notification") + case .error: + title = NSLocalizedString("Encrypted message: database error", comment: "notification") + case .errorKeychain: + title = NSLocalizedString("Encrypted message: keychain error", comment: "notification") + case .unknown: + title = NSLocalizedString("Encrypted message: unexpexted error", comment: "notification") + case .ok: + title = NSLocalizedString("Encrypted message or another event", comment: "notification") + } + return createNotification( + categoryIdentifier: ntfCategoryConnectionEvent, + title: title + ) +} + private func groupMsgNtfTitle(_ groupInfo: GroupInfo, _ groupMember: GroupMember, hideContent: Bool) -> String { hideContent ? NSLocalizedString("Group message:", comment: "notification") From 22ee465d3b5c0f6a0b5cc8b9868d422d4c005581 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 8 Sep 2022 07:35:32 +0100 Subject: [PATCH 24/44] nix: update to rename libs dependencies and to remove libunwind from .so files --- flake.nix | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index a56a295d3b..eac09fc197 100644 --- a/flake.nix +++ b/flake.nix @@ -116,11 +116,25 @@ # find ${androidPkgs.gmp6.override { withStatic = true; }}/lib -name "*.a" -exec cp {} $out/_pkg \; # find ${androidIconv}/lib -name "*.a" -exec cp {} $out/_pkg \; # find ${androidPkgs.stdenv.cc.libc}/lib -name "*.a" -exec cp {} $out/_pkg \; - echo "androidPkgs" - ls -lha ${androidPkgs} + echo ${androidPkgs.openssl} find ${androidPkgs.openssl.out}/lib -name "*.so" -exec cp {} $out/_pkg \; - ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsimplex.so + # remove the .1 and other version suffixes from .so's. Androids linker + # doesn't play nice with them. + for lib in $out/_pkg/*.so; do + for dep in $(${pkgs.patchelf}/bin/patchelf --print-needed "$lib"); do + if [[ "''${dep##*.so}" ]]; then + echo "$lib : $dep -> ''${dep%%.so*}.so" + chmod +w "$lib" + ${pkgs.patchelf}/bin/patchelf --replace-needed "$dep" "''${dep%%.so*}.so" "$lib" + fi + done + done + + for lib in $out/_pkg/*.so; do + chmod +w "$lib" + ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so "$lib" + done ${pkgs.tree}/bin/tree $out/_pkg (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-aarch64-android-libsimplex.zip *) From 9eb244f9c1b6c39d8a0ca9b7cbdfe65fd459670e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 8 Sep 2022 14:04:33 +0100 Subject: [PATCH 25/44] nix: set so name --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index eac09fc197..3087a5663a 100644 --- a/flake.nix +++ b/flake.nix @@ -134,6 +134,7 @@ for lib in $out/_pkg/*.so; do chmod +w "$lib" ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so "$lib" + ${pkgs.patchelf}/bin/patchelf --set-soname $lib $lib done ${pkgs.tree}/bin/tree $out/_pkg From 06835ee3fc1cd6243da1dca206906ad208e9fe54 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 8 Sep 2022 17:36:16 +0100 Subject: [PATCH 26/44] ios: additional db encryption UX (#1031) * ios: additional db encryption UX * typo Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> * fixes Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> --- .../Database/DatabaseEncryptionView.swift | 10 +-- .../Views/Database/DatabaseErrorView.swift | 68 ++++++++++++++----- .../Shared/Views/Database/DatabaseView.swift | 21 +++++- apps/ios/Shared/Views/TerminalView.swift | 20 ++++++ src/Simplex/Chat.hs | 2 +- tests/ChatTests.hs | 4 +- 6 files changed, 98 insertions(+), 27 deletions(-) diff --git a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift index 374133355b..7358d9a45b 100644 --- a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift @@ -35,9 +35,9 @@ enum DatabaseEncryptionAlert: Identifiable { struct DatabaseEncryptionView: View { @EnvironmentObject private var m: ChatModel + @Binding var useKeychain: Bool @State private var alert: DatabaseEncryptionAlert? = nil @State private var progressIndicator = false - @State private var useKeychain = storeDBPassphraseGroupDefault.get() @State private var useKeychainToggle = storeDBPassphraseGroupDefault.get() @State private var initialRandomDBPassphrase = initialRandomDBPassphraseGroupDefault.get() @State private var storedKey = getDatabaseKey() != nil @@ -58,7 +58,7 @@ struct DatabaseEncryptionView: View { private func databaseEncryptionView() -> some View { List { Section { - settingsRow("key") { + settingsRow(storedKey ? "key.fill" : "key", color: storedKey ? .green : .secondary) { Toggle("Save passphrase in Keychain", isOn: $useKeychainToggle) .onChange(of: useKeychainToggle) { _ in if useKeychainToggle { @@ -224,7 +224,7 @@ struct DatabaseEncryptionView: View { case .currentPassphraseError: return Alert( title: Text("Wrong passsphrase!"), - message: Text("Please enter correct current passphrase") + message: Text("Please enter correct current passphrase.") ) case let .error(title, error): return Alert(title: Text(title), message: Text("\(error)")) @@ -254,6 +254,7 @@ struct DatabaseKeyField: View { var placeholder: LocalizedStringKey var valid: Bool var showStrength = false + var onSubmit: () -> Void = {} @State private var showKey = false var body: some View { @@ -272,6 +273,7 @@ struct DatabaseKeyField: View { .autocapitalization(.none) .submitLabel(.done) .padding(.leading, 36) + .onSubmit(onSubmit) } } @@ -338,6 +340,6 @@ func validKey(_ s: String) -> Bool { struct DatabaseEncryptionView_Previews: PreviewProvider { static var previews: some View { - DatabaseEncryptionView() + DatabaseEncryptionView(useKeychain: Binding.constant(true)) } } diff --git a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift index 310c0d96ea..9b3c8c0fb0 100644 --- a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift @@ -11,7 +11,7 @@ import SimpleXChat struct DatabaseErrorView: View { @EnvironmentObject var m: ChatModel - var status: DBMigrationResult + @State var status: DBMigrationResult @State private var dbKey = "" @State private var storedDBKey = getDatabaseKey() @State private var useKeychain = storeDBPassphraseGroupDefault.get() @@ -23,17 +23,18 @@ struct DatabaseErrorView: View { if useKeychain && storedDBKey != nil && storedDBKey != "" { Text("Wrong database passphrase").font(.title) Text("Database passphrase is different from saved in the keychain.") - DatabaseKeyField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey)) + databaseKeyField(onSubmit: saveAndRunChat) saveAndOpenButton() Spacer() Text("File: \(dbFile)") } else { Text("Encrypted database").font(.title) Text("Database passphrase is required to open chat.") - DatabaseKeyField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey)) if useKeychain { + databaseKeyField(onSubmit: saveAndRunChat) saveAndOpenButton() } else { + databaseKeyField(onSubmit: runChat) openChatButton() } Spacer() @@ -59,29 +60,62 @@ struct DatabaseErrorView: View { } } .padding() - .frame(maxHeight: .infinity) } + .frame(maxHeight: .infinity) + } + + private func databaseKeyField(onSubmit: @escaping () -> Void) -> some View { + DatabaseKeyField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey), onSubmit: onSubmit) + } private func saveAndOpenButton() -> some View { Button("Save passphrase and open chat") { - if setDatabaseKey(dbKey) { - storeDBPassphraseGroupDefault.set(true) - initialRandomDBPassphraseGroupDefault.set(false) - } - do { - try initializeChat(start: m.v3DBMigration.startChat, dbKey: dbKey) - } catch let error { - logger.error("initializeChat \(responseError(error))") - } + saveAndRunChat() } } private func openChatButton() -> some View { Button("Open chat") { - do { - try initializeChat(start: m.v3DBMigration.startChat, dbKey: dbKey) - } catch let error { - logger.error("initializeChat \(responseError(error))") + runChat() + } + } + + private func saveAndRunChat() { + if setDatabaseKey(dbKey) { + storeDBPassphraseGroupDefault.set(true) + initialRandomDBPassphraseGroupDefault.set(false) + } + runChat() + } + + private func runChat() { + do { + try initializeChat(start: m.v3DBMigration.startChat, dbKey: dbKey) + if let s = m.chatDbStatus { + status = s + let am = AlertManager.shared + switch s { + case .errorNotADatabase: + am.showAlertMsg( + title: "Wrong passphrase!", + message: "Enter correct passphrase." + ) + case .errorKeychain: + am.showAlertMsg(title: "Keychain error") + case let .error(_, error): + am.showAlert(Alert( + title: Text("Database error"), + message: Text(error) + )) + case let .unknown(error): + am.showAlert(Alert( + title: Text("Unknown error"), + message: Text(error) + )) + case .ok: () + } } + } catch let error { + logger.error("initializeChat \(responseError(error))") } } } diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 8f7ba7cca4..5fe3b7b836 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -45,6 +45,7 @@ struct DatabaseView: View { @AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0 @State private var dbContainer = dbContainerGroupDefault.get() @State private var legacyDatabase = hasLegacyDatabase() + @State private var useKeychain = storeDBPassphraseGroupDefault.get() var body: some View { ZStack { @@ -86,9 +87,9 @@ struct DatabaseView: View { Section { let unencrypted = m.chatDbEncrypted == false let color: Color = unencrypted ? .orange : .secondary - settingsRow(unencrypted ? "lock.open" : "lock", color: color) { + settingsRow(unencrypted ? "lock.open" : useKeychain ? "key" : "lock", color: color) { NavigationLink { - DatabaseEncryptionView() + DatabaseEncryptionView(useKeychain: $useKeychain) .navigationTitle("Database passphrase") } label: { Text("Database passphrase") @@ -168,7 +169,7 @@ struct DatabaseView: View { title: Text("Stop chat?"), message: Text("Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped."), primaryButton: .destructive(Text("Stop")) { - stopChat() + authStopChat() }, secondaryButton: .cancel { withAnimation { runChat = true } @@ -226,6 +227,20 @@ struct DatabaseView: View { } } + private func authStopChat() { + if UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) { + authenticate(reason: NSLocalizedString("Stop SimpleX", comment: "authentication reason")) { laResult in + switch laResult { + case .success: stopChat() + case .unavailable: stopChat() + case .failed: withAnimation { runChat = true } + } + } + } else { + stopChat() + } + } + private func stopChat() { Task { do { diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index 6aa9e8804c..488602dda6 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -17,8 +17,28 @@ struct TerminalView: View { @EnvironmentObject var chatModel: ChatModel @State var composeState: ComposeState = ComposeState() @FocusState private var keyboardVisible: Bool + @State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) var body: some View { + if authorized { + terminalView() + } else { + Button(action: runAuth) { Label("Unlock", systemImage: "lock") } + .onAppear(perform: runAuth) + } + } + + private func runAuth() { + authenticate(reason: NSLocalizedString("Open chat console", comment: "authentication reason")) { laResult in + switch laResult { + case .success: authorized = true + case .unavailable: authorized = true + case .failed: authorized = false + } + } + } + + private func terminalView() -> some View { VStack { ScrollViewReader { proxy in ScrollView { diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 70d0272c55..65670eb262 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -2546,7 +2546,7 @@ chatCommandP = "/_db delete" $> APIDeleteStorage, "/_db encryption " *> (APIStorageEncryption <$> jsonP), "/db encrypt " *> (APIStorageEncryption . DBEncryptionConfig "" <$> dbKeyP), - "/db password " *> (APIStorageEncryption <$> (DBEncryptionConfig <$> dbKeyP <* A.space <*> dbKeyP)), + "/db key " *> (APIStorageEncryption <$> (DBEncryptionConfig <$> dbKeyP <* A.space <*> dbKeyP)), "/db decrypt " *> (APIStorageEncryption . (`DBEncryptionConfig` "") <$> dbKeyP), "/_get chats" *> (APIGetChats <$> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False)), "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP <*> optional searchP), diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 983258fd44..1325db5620 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -2788,9 +2788,9 @@ testDatabaseEncryption = withTmpFiles $ do testChatWorking alice bob alice ##> "/_stop" alice <## "chat stopped" - alice ##> "/db password wrongkey nextkey" + alice ##> "/db key wrongkey nextkey" alice <## "error encrypting database: wrong passphrase or invalid database file" - alice ##> "/db password mykey nextkey" + alice ##> "/db key mykey nextkey" alice <## "ok" alice ##> "/_db encryption {\"currentKey\":\"nextkey\",\"newKey\":\"anotherkey\"}" alice <## "ok" From 8085515f56735811298552effd5bccd895c920b0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 10 Sep 2022 11:08:28 +0100 Subject: [PATCH 27/44] nix: set -x --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index 3087a5663a..23a5666fca 100644 --- a/flake.nix +++ b/flake.nix @@ -103,6 +103,7 @@ # template haskell cross compilation. Thus we just pass them as linker options (-optl). setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsimplex.so" "-optl-lHSrts_thr" "-optl-lffi"]; postInstall = '' + set -x ${pkgs.tree}/bin/tree $out mkdir -p $out/_pkg # copy over includes, we might want those, but maybe not. From 33011b5d48d4abbf3f277e88929d01e7fc8c3c9c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 10 Sep 2022 14:17:08 +0100 Subject: [PATCH 28/44] nix: skip libsimplex.so when patching so name --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 23a5666fca..36afef08e3 100644 --- a/flake.nix +++ b/flake.nix @@ -135,7 +135,7 @@ for lib in $out/_pkg/*.so; do chmod +w "$lib" ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so "$lib" - ${pkgs.patchelf}/bin/patchelf --set-soname $lib $lib + [[ "$lib" != *libsimplex.so ]] ${pkgs.patchelf}/bin/patchelf --set-soname $lib $lib done ${pkgs.tree}/bin/tree $out/_pkg From e5e8d95ba4e603a0aa953e1987bd5bb0bc4ec036 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 10 Sep 2022 16:12:34 +0100 Subject: [PATCH 29/44] nix: fix condition syntax --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 36afef08e3..e0ad7524fe 100644 --- a/flake.nix +++ b/flake.nix @@ -135,7 +135,7 @@ for lib in $out/_pkg/*.so; do chmod +w "$lib" ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so "$lib" - [[ "$lib" != *libsimplex.so ]] ${pkgs.patchelf}/bin/patchelf --set-soname $lib $lib + [[ "$lib" != *libsimplex.so ]] && ${pkgs.patchelf}/bin/patchelf --set-soname $lib $lib done ${pkgs.tree}/bin/tree $out/_pkg From ca0a51a4851c904d6ce20c7470aa692934ccc184 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 10 Sep 2022 18:02:19 +0100 Subject: [PATCH 30/44] nix: add commoncrypto flag to tagged json builds --- flake.nix | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/flake.nix b/flake.nix index e8633a5d0b..d442d3d6c6 100644 --- a/flake.nix +++ b/flake.nix @@ -277,7 +277,7 @@ packages.simplexmq.flags.swift = true; packages.direct-sqlcipher.flags.commoncrypto = true; }]; - } ).simplex-chat.components.library.override { + }).simplex-chat.components.library.override { smallAddressSpace = true; enableShared = false; # we need threaded here, otherwise all the queing logic doesn't work properly. # for iOS we also use -staticlib, to get one rolled up library. @@ -286,7 +286,12 @@ postInstall = iosPostInstall "pkg-ios-aarch64-swift-json"; }; # This is the aarch64-darwin build with tagged JSON format (for Mac & Flutter) - "aarch64-darwin:lib:simplex-chat" = (drv pkgs).simplex-chat.components.library.override { + "aarch64-darwin:lib:simplex-chat" = (drv' { + pkgs' = pkgs; + extra-modules = [{ + packages.direct-sqlcipher.flags.commoncrypto = true; + }]; + }).simplex-chat.components.library.override { smallAddressSpace = true; enableShared = false; # we need threaded here, otherwise all the queing logic doesn't work properly. # for iOS we also use -staticlib, to get one rolled up library. @@ -303,7 +308,7 @@ packages.simplexmq.flags.swift = true; packages.direct-sqlcipher.flags.commoncrypto = true; }]; - } ).simplex-chat.components.library.override { + }).simplex-chat.components.library.override { smallAddressSpace = true; enableShared = false; # we need threaded here, otherwise all the queing logic doesn't work properly. # for iOS we also use -staticlib, to get one rolled up library. @@ -312,7 +317,12 @@ postInstall = iosPostInstall "pkg-ios-x86_64-swift-json"; }; # This is the aarch64-darwin build with tagged JSON format (for Mac & Flutter) - "x86_64-darwin:lib:simplex-chat" = (drv pkgs).simplex-chat.components.library.override { + "x86_64-darwin:lib:simplex-chat" = (drv' { + pkgs' = pkgs; + extra-modules = [{ + packages.direct-sqlcipher.flags.commoncrypto = true; + }]; + }).simplex-chat.components.library.override { smallAddressSpace = true; enableShared = false; # we need threaded here, otherwise all the queing logic doesn't work properly. # for iOS we also use -staticlib, to get one rolled up library. From 0c716da346fc7a2cbf95d3ad96963651053fb5f8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 10 Sep 2022 18:18:47 +0100 Subject: [PATCH 31/44] nix: fix patchelf --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index d442d3d6c6..08a1e18dd2 100644 --- a/flake.nix +++ b/flake.nix @@ -153,7 +153,7 @@ for lib in $out/_pkg/*.so; do chmod +w "$lib" ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so "$lib" - [[ "$lib" != *libsimplex.so ]] && ${pkgs.patchelf}/bin/patchelf --set-soname $lib $lib + [[ "$lib" != *libsimplex.so ]] && ${pkgs.patchelf}/bin/patchelf --set-soname "$(basename -a $lib)" "$lib" done ${pkgs.tree}/bin/tree $out/_pkg From 2a32810182ddf0f2eac93db87ea7ee8ab3e3dfae Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 10 Sep 2022 21:49:51 +0100 Subject: [PATCH 32/44] nix: android/log.h patch --- flake.nix | 5 +- scripts/nix/direct-sqlcipher-android.patch | 324 +++++++++++++++++++++ 2 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 scripts/nix/direct-sqlcipher-android.patch diff --git a/flake.nix b/flake.nix index 08a1e18dd2..1b1d494f57 100644 --- a/flake.nix +++ b/flake.nix @@ -25,7 +25,10 @@ }; sha256map = import ./scripts/nix/sha256map.nix; modules = [{ - packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-2.3.27.patch ]; + packages.direct-sqlcipher.patches = [ + "./scripts/nix/direct-sqlcipher-2.3.27.patch", + "./scripts/nix/direct-sqlcipher-android.patch" + ]; packages.entropy.patches = [ ./scripts/nix/entropy.patch ]; } ({ pkgs,lib, ... }: lib.mkIf (pkgs.stdenv.hostPlatform.isAndroid) { diff --git a/scripts/nix/direct-sqlcipher-android.patch b/scripts/nix/direct-sqlcipher-android.patch new file mode 100644 index 0000000000..225172e4f2 --- /dev/null +++ b/scripts/nix/direct-sqlcipher-android.patch @@ -0,0 +1,324 @@ +diff --git a/cbits/android/log.h b/cbits/android/log.h +new file mode 100644 +index 0000000..c8e715e +--- /dev/null ++++ b/cbits/android/log.h +@@ -0,0 +1,318 @@ ++/* ++ * Copyright (C) 2009 The Android Open Source Project ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); ++ * you may not use this file except in compliance with the License. ++ * You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++#pragma once ++/** ++ * @addtogroup Logging ++ * @{ ++ */ ++/** ++ * \file ++ * ++ * Support routines to send messages to the Android log buffer, ++ * which can later be accessed through the `logcat` utility. ++ * ++ * Each log message must have ++ * - a priority ++ * - a log tag ++ * - some text ++ * ++ * The tag normally corresponds to the component that emits the log message, ++ * and should be reasonably small. ++ * ++ * Log message text may be truncated to less than an implementation-specific ++ * limit (1023 bytes). ++ * ++ * Note that a newline character ("\n") will be appended automatically to your ++ * log message, if not already there. It is not possible to send several ++ * messages and have them appear on a single line in logcat. ++ * ++ * Please use logging in moderation: ++ * ++ * - Sending log messages eats CPU and slow down your application and the ++ * system. ++ * ++ * - The circular log buffer is pretty small, so sending many messages ++ * will hide other important log messages. ++ * ++ * - In release builds, only send log messages to account for exceptional ++ * conditions. ++ */ ++#include ++#include ++#include ++#include ++#if !defined(__BIONIC__) && !defined(__INTRODUCED_IN) ++#define __INTRODUCED_IN(x) ++#endif ++#ifdef __cplusplus ++extern "C" { ++#endif ++/** ++ * Android log priority values, in increasing order of priority. ++ */ ++typedef enum android_LogPriority { ++ /** For internal use only. */ ++ ANDROID_LOG_UNKNOWN = 0, ++ /** The default priority, for internal use only. */ ++ ANDROID_LOG_DEFAULT, /* only for SetMinPriority() */ ++ /** Verbose logging. Should typically be disabled for a release apk. */ ++ ANDROID_LOG_VERBOSE, ++ /** Debug logging. Should typically be disabled for a release apk. */ ++ ANDROID_LOG_DEBUG, ++ /** Informational logging. Should typically be disabled for a release apk. */ ++ ANDROID_LOG_INFO, ++ /** Warning logging. For use with recoverable failures. */ ++ ANDROID_LOG_WARN, ++ /** Error logging. For use with unrecoverable failures. */ ++ ANDROID_LOG_ERROR, ++ /** Fatal logging. For use when aborting. */ ++ ANDROID_LOG_FATAL, ++ /** For internal use only. */ ++ ANDROID_LOG_SILENT, /* only for SetMinPriority(); must be last */ ++} android_LogPriority; ++/** ++ * Writes the constant string `text` to the log, with priority `prio` and tag ++ * `tag`. ++ */ ++int __android_log_write(int prio, const char* tag, const char* text); ++/** ++ * Writes a formatted string to the log, with priority `prio` and tag `tag`. ++ * The details of formatting are the same as for ++ * [printf(3)](http://man7.org/linux/man-pages/man3/printf.3.html). ++ */ ++int __android_log_print(int prio, const char* tag, const char* fmt, ...) ++ __attribute__((__format__(printf, 3, 4))); ++/** ++ * Equivalent to `__android_log_print`, but taking a `va_list`. ++ * (If `__android_log_print` is like `printf`, this is like `vprintf`.) ++ */ ++int __android_log_vprint(int prio, const char* tag, const char* fmt, va_list ap) ++ __attribute__((__format__(printf, 3, 0))); ++/** ++ * Writes an assertion failure to the log (as `ANDROID_LOG_FATAL`) and to ++ * stderr, before calling ++ * [abort(3)](http://man7.org/linux/man-pages/man3/abort.3.html). ++ * ++ * If `fmt` is non-null, `cond` is unused. If `fmt` is null, the string ++ * `Assertion failed: %s` is used with `cond` as the string argument. ++ * If both `fmt` and `cond` are null, a default string is provided. ++ * ++ * Most callers should use ++ * [assert(3)](http://man7.org/linux/man-pages/man3/assert.3.html) from ++ * `<assert.h>` instead, or the `__assert` and `__assert2` functions ++ * provided by bionic if more control is needed. They support automatically ++ * including the source filename and line number more conveniently than this ++ * function. ++ */ ++void __android_log_assert(const char* cond, const char* tag, const char* fmt, ...) ++ __attribute__((__noreturn__)) __attribute__((__format__(printf, 3, 4))); ++/** ++ * Identifies a specific log buffer for __android_log_buf_write() ++ * and __android_log_buf_print(). ++ */ ++typedef enum log_id { ++ LOG_ID_MIN = 0, ++ /** The main log buffer. This is the only log buffer available to apps. */ ++ LOG_ID_MAIN = 0, ++ /** The radio log buffer. */ ++ LOG_ID_RADIO = 1, ++ /** The event log buffer. */ ++ LOG_ID_EVENTS = 2, ++ /** The system log buffer. */ ++ LOG_ID_SYSTEM = 3, ++ /** The crash log buffer. */ ++ LOG_ID_CRASH = 4, ++ /** The statistics log buffer. */ ++ LOG_ID_STATS = 5, ++ /** The security log buffer. */ ++ LOG_ID_SECURITY = 6, ++ /** The kernel log buffer. */ ++ LOG_ID_KERNEL = 7, ++ LOG_ID_MAX, ++ /** Let the logging function choose the best log target. */ ++ LOG_ID_DEFAULT = 0x7FFFFFFF ++} log_id_t; ++/** ++ * Writes the constant string `text` to the log buffer `id`, ++ * with priority `prio` and tag `tag`. ++ * ++ * Apps should use __android_log_write() instead. ++ */ ++int __android_log_buf_write(int bufID, int prio, const char* tag, const char* text); ++/** ++ * Writes a formatted string to log buffer `id`, ++ * with priority `prio` and tag `tag`. ++ * The details of formatting are the same as for ++ * [printf(3)](http://man7.org/linux/man-pages/man3/printf.3.html). ++ * ++ * Apps should use __android_log_print() instead. ++ */ ++int __android_log_buf_print(int bufID, int prio, const char* tag, const char* fmt, ...) ++ __attribute__((__format__(printf, 4, 5))); ++/** ++ * Logger data struct used for writing log messages to liblog via __android_log_write_logger_data() ++ * and sending log messages to user defined loggers specified in __android_log_set_logger(). ++ */ ++struct __android_log_message { ++ size_t ++ struct_size; /** Must be set to sizeof(__android_log_message) and is used for versioning. */ ++ int32_t buffer_id; /** {@link log_id_t} values. */ ++ int32_t priority; /** {@link android_LogPriority} values. */ ++ const char* tag; /** The tag for the log message. */ ++ const char* file; /** Optional file name, may be set to nullptr. */ ++ uint32_t line; /** Optional line number, ignore if file is nullptr. */ ++ const char* message; /** The log message itself. */ ++}; ++/** ++ * Prototype for the 'logger' function that is called for every log message. ++ */ ++typedef void (*__android_logger_function)(const struct __android_log_message* log_message); ++/** ++ * Prototype for the 'abort' function that is called when liblog will abort due to ++ * __android_log_assert() failures. ++ */ ++typedef void (*__android_aborter_function)(const char* abort_message); ++#if !defined(__ANDROID__) || __ANDROID_API__ >= 30 ++/** ++ * Writes the log message specified by log_message. log_message includes additional file name and ++ * line number information that a logger may use. log_message is versioned for backwards ++ * compatibility. ++ * This assumes that loggability has already been checked through __android_log_is_loggable(). ++ * Higher level logging libraries, such as libbase, first check loggability, then format their ++ * buffers, then pass the message to liblog via this function, and therefore we do not want to ++ * duplicate the loggability check here. ++ * ++ * @param log_message the log message itself, see {@link __android_log_message}. ++ * ++ * Available since API level 30. ++ */ ++void __android_log_write_log_message(struct __android_log_message* log_message) __INTRODUCED_IN(30); ++/** ++ * Sets a user defined logger function. All log messages sent to liblog will be set to the ++ * function pointer specified by logger for processing. It is not expected that log messages are ++ * already terminated with a new line. This function should add new lines if required for line ++ * separation. ++ * ++ * @param logger the new function that will handle log messages. ++ * ++ * Available since API level 30. ++ */ ++void __android_log_set_logger(__android_logger_function logger) __INTRODUCED_IN(30); ++/** ++ * Writes the log message to logd. This is an __android_logger_function and can be provided to ++ * __android_log_set_logger(). It is the default logger when running liblog on a device. ++ * ++ * @param log_message the log message to write, see {@link __android_log_message}. ++ * ++ * Available since API level 30. ++ */ ++void __android_log_logd_logger(const struct __android_log_message* log_message) __INTRODUCED_IN(30); ++/** ++ * Writes the log message to stderr. This is an __android_logger_function and can be provided to ++ * __android_log_set_logger(). It is the default logger when running liblog on host. ++ * ++ * @param log_message the log message to write, see {@link __android_log_message}. ++ * ++ * Available since API level 30. ++ */ ++void __android_log_stderr_logger(const struct __android_log_message* log_message) ++ __INTRODUCED_IN(30); ++/** ++ * Sets a user defined aborter function that is called for __android_log_assert() failures. This ++ * user defined aborter function is highly recommended to abort and be noreturn, but is not strictly ++ * required to. ++ * ++ * @param aborter the new aborter function, see {@link __android_aborter_function}. ++ * ++ * Available since API level 30. ++ */ ++void __android_log_set_aborter(__android_aborter_function aborter) __INTRODUCED_IN(30); ++/** ++ * Calls the stored aborter function. This allows for other logging libraries to use the same ++ * aborter function by calling this function in liblog. ++ * ++ * @param abort_message an additional message supplied when aborting, for example this is used to ++ * call android_set_abort_message() in __android_log_default_aborter(). ++ * ++ * Available since API level 30. ++ */ ++void __android_log_call_aborter(const char* abort_message) __INTRODUCED_IN(30); ++/** ++ * Sets android_set_abort_message() on device then aborts(). This is the default aborter. ++ * ++ * @param abort_message an additional message supplied when aborting. This functions calls ++ * android_set_abort_message() with its contents. ++ * ++ * Available since API level 30. ++ */ ++void __android_log_default_aborter(const char* abort_message) __attribute__((noreturn)) ++__INTRODUCED_IN(30); ++/** ++ * Use the per-tag properties "log.tag." along with the minimum priority from ++ * __android_log_set_minimum_priority() to determine if a log message with a given prio and tag will ++ * be printed. A non-zero result indicates yes, zero indicates false. ++ * ++ * If both a priority for a tag and a minimum priority are set by ++ * __android_log_set_minimum_priority(), then the lowest of the two values are to determine the ++ * minimum priority needed to log. If only one is set, then that value is used to determine the ++ * minimum priority needed. If none are set, then default_priority is used. ++ * ++ * @param prio the priority to test, takes {@link android_LogPriority} values. ++ * @param tag the tag to test. ++ * @param len the length of the tag. ++ * @param default_prio the default priority to use if no properties or minimum priority are set. ++ * @return an integer where 1 indicates that the message is loggable and 0 indicates that it is not. ++ * ++ * Available since API level 30. ++ */ ++int __android_log_is_loggable(int prio, const char* tag, int default_prio) __INTRODUCED_IN(30); ++int __android_log_is_loggable_len(int prio, const char* tag, size_t len, int default_prio) ++ __INTRODUCED_IN(30); ++/** ++ * Sets the minimum priority that will be logged for this process. ++ * ++ * @param priority the new minimum priority to set, takes @{link android_LogPriority} values. ++ * @return the previous set minimum priority as @{link android_LogPriority} values, or ++ * ANDROID_LOG_DEFAULT if none was set. ++ * ++ * Available since API level 30. ++ */ ++int32_t __android_log_set_minimum_priority(int32_t priority) __INTRODUCED_IN(30); ++/** ++ * Gets the minimum priority that will be logged for this process. If none has been set by a ++ * previous __android_log_set_minimum_priority() call, this returns ANDROID_LOG_DEFAULT. ++ * ++ * @return the current minimum priority as @{link android_LogPriority} values, or ++ * ANDROID_LOG_DEFAULT if none is set. ++ * ++ * Available since API level 30. ++ */ ++int32_t __android_log_get_minimum_priority(void) __INTRODUCED_IN(30); ++/** ++ * Sets the default tag if no tag is provided when writing a log message. Defaults to ++ * getprogname(). This truncates tag to the maximum log message size, though appropriate tags ++ * should be much smaller. ++ * ++ * @param tag the new log tag. ++ * ++ * Available since API level 30. ++ */ ++void __android_log_set_default_tag(const char* tag) __INTRODUCED_IN(30); ++#endif ++#ifdef __cplusplus ++} ++#endif ++/** @} */ From 35af0786c0f5fbcfe7713c443c645cc2a09efb54 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 10 Sep 2022 21:55:26 +0100 Subject: [PATCH 33/44] nix: fix syntax error --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 1b1d494f57..3098f2ade8 100644 --- a/flake.nix +++ b/flake.nix @@ -26,7 +26,7 @@ sha256map = import ./scripts/nix/sha256map.nix; modules = [{ packages.direct-sqlcipher.patches = [ - "./scripts/nix/direct-sqlcipher-2.3.27.patch", + "./scripts/nix/direct-sqlcipher-2.3.27.patch" "./scripts/nix/direct-sqlcipher-android.patch" ]; packages.entropy.patches = [ ./scripts/nix/entropy.patch ]; From 9d70cf1e7bf99b690dbe8c369d41d2e4f604cfa2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 10 Sep 2022 22:10:26 +0100 Subject: [PATCH 34/44] nix: update patches --- flake.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 3098f2ade8..056735f4a4 100644 --- a/flake.nix +++ b/flake.nix @@ -26,8 +26,8 @@ sha256map = import ./scripts/nix/sha256map.nix; modules = [{ packages.direct-sqlcipher.patches = [ - "./scripts/nix/direct-sqlcipher-2.3.27.patch" - "./scripts/nix/direct-sqlcipher-android.patch" + ./scripts/nix/direct-sqlcipher-2.3.27.patch + ./scripts/nix/direct-sqlcipher-android.patch ]; packages.entropy.patches = [ ./scripts/nix/entropy.patch ]; } From f6e2f11299cf3b0efbc55509a09b73fba3440b19 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 10 Sep 2022 23:02:37 +0100 Subject: [PATCH 35/44] nix: patch to replace ERR_error_string with ERR_func_error_string --- flake.nix | 1 + scripts/nix/direct-sqlcipher-err-string.patch | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 scripts/nix/direct-sqlcipher-err-string.patch diff --git a/flake.nix b/flake.nix index 056735f4a4..0427d1ce7b 100644 --- a/flake.nix +++ b/flake.nix @@ -28,6 +28,7 @@ packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-2.3.27.patch ./scripts/nix/direct-sqlcipher-android.patch + ./scripts/nix/direct-sqlcipher-err-string.patch ]; packages.entropy.patches = [ ./scripts/nix/entropy.patch ]; } diff --git a/scripts/nix/direct-sqlcipher-err-string.patch b/scripts/nix/direct-sqlcipher-err-string.patch new file mode 100644 index 0000000000..c12c729acb --- /dev/null +++ b/scripts/nix/direct-sqlcipher-err-string.patch @@ -0,0 +1,12 @@ +diff --git a/cbits/sqlite3.c b/cbits/sqlite3.c +index 66bb609..ca32723 100644 +--- a/cbits/sqlite3.c ++++ b/cbits/sqlite3.c +@@ -105731,7 +105731,7 @@ static unsigned int openssl_init_count = 0; + static void sqlcipher_openssl_log_errors() { + unsigned long err = 0; + while((err = ERR_get_error()) != 0) { +- sqlcipher_log(SQLCIPHER_LOG_ERROR, "sqlcipher_openssl_log_errors: ERR_get_error() returned %lx: %s", err, ERR_error_string(err, NULL)); ++ sqlcipher_log(SQLCIPHER_LOG_ERROR, "sqlcipher_openssl_log_errors: ERR_get_error() returned %lx: %s", err, ERR_func_error_string(err)); + } + } From e080690c2e5ce9af51e60e146a8c4702880daa60 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 11 Sep 2022 09:23:16 +0100 Subject: [PATCH 36/44] nix: remove ERR_error_string patch --- flake.nix | 1 - scripts/nix/direct-sqlcipher-err-string.patch | 12 ------------ 2 files changed, 13 deletions(-) delete mode 100644 scripts/nix/direct-sqlcipher-err-string.patch diff --git a/flake.nix b/flake.nix index 0427d1ce7b..056735f4a4 100644 --- a/flake.nix +++ b/flake.nix @@ -28,7 +28,6 @@ packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-2.3.27.patch ./scripts/nix/direct-sqlcipher-android.patch - ./scripts/nix/direct-sqlcipher-err-string.patch ]; packages.entropy.patches = [ ./scripts/nix/entropy.patch ]; } diff --git a/scripts/nix/direct-sqlcipher-err-string.patch b/scripts/nix/direct-sqlcipher-err-string.patch deleted file mode 100644 index c12c729acb..0000000000 --- a/scripts/nix/direct-sqlcipher-err-string.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/cbits/sqlite3.c b/cbits/sqlite3.c -index 66bb609..ca32723 100644 ---- a/cbits/sqlite3.c -+++ b/cbits/sqlite3.c -@@ -105731,7 +105731,7 @@ static unsigned int openssl_init_count = 0; - static void sqlcipher_openssl_log_errors() { - unsigned long err = 0; - while((err = ERR_get_error()) != 0) { -- sqlcipher_log(SQLCIPHER_LOG_ERROR, "sqlcipher_openssl_log_errors: ERR_get_error() returned %lx: %s", err, ERR_error_string(err, NULL)); -+ sqlcipher_log(SQLCIPHER_LOG_ERROR, "sqlcipher_openssl_log_errors: ERR_get_error() returned %lx: %s", err, ERR_func_error_string(err)); - } - } From 69138a24de98d184ab0b58ff0f43004c8e4503bd Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 13 Sep 2022 08:38:31 +0100 Subject: [PATCH 37/44] nix: patch out android logging from sqlcipher --- flake.nix | 12 +- .../nix/direct-sqlcipher-android-log.patch | 68 ++++ scripts/nix/direct-sqlcipher-android.patch | 324 ------------------ 3 files changed, 76 insertions(+), 328 deletions(-) create mode 100644 scripts/nix/direct-sqlcipher-android-log.patch delete mode 100644 scripts/nix/direct-sqlcipher-android.patch diff --git a/flake.nix b/flake.nix index 8e3396b296..007a9b117b 100644 --- a/flake.nix +++ b/flake.nix @@ -25,10 +25,7 @@ }; sha256map = import ./scripts/nix/sha256map.nix; modules = [{ - packages.direct-sqlcipher.patches = [ - ./scripts/nix/direct-sqlcipher-2.3.27.patch - ./scripts/nix/direct-sqlcipher-android.patch - ]; + packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-2.3.27.patch ]; packages.entropy.patches = [ ./scripts/nix/entropy.patch ]; } ({ pkgs,lib, ... }: lib.mkIf (pkgs.stdenv.hostPlatform.isAndroid) { @@ -125,6 +122,13 @@ pkgs' = androidPkgs; extra-modules = [{ packages.direct-sqlcipher.flags.openssl = true; + packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [ + (androidPkgs.openssl.override { static = true; }) + ]; + packages.direct-sqlcipher.patches = [ + ./scripts/nix/direct-sqlcipher-2.3.27.patch + ./scripts/nix/direct-sqlcipher-android-log.patch + ]; }]; }).simplex-chat.components.library.override { smallAddressSpace = true; enableShared = false; diff --git a/scripts/nix/direct-sqlcipher-android-log.patch b/scripts/nix/direct-sqlcipher-android-log.patch new file mode 100644 index 0000000000..e6df2f7942 --- /dev/null +++ b/scripts/nix/direct-sqlcipher-android-log.patch @@ -0,0 +1,68 @@ +diff --git a/cbits/sqlite3.c b/cbits/sqlite3.c +index 66bb609..00c33c1 100644 +--- a/cbits/sqlite3.c ++++ b/cbits/sqlite3.c +@@ -101739,9 +101739,9 @@ sqlite3_mutex* sqlcipher_mutex(int); + /* #include "pager.h" */ + /* #include "vdbeInt.h" */ + +-#ifdef __ANDROID__ +-#include +-#endif ++// #ifdef __ANDROID__ ++// #include ++// #endif + + /* #include */ + +@@ -104934,11 +104934,11 @@ static int sqlcipher_profile_callback(unsigned int trace, void *file, void *stmt + FILE *f = (FILE*) file; + char *fmt = "Elapsed time:%.3f ms - %s\n"; + double elapsed = (*((sqlite3_uint64*)run_time))/1000000.0; +-#ifdef __ANDROID__ +- if(f == NULL) { +- __android_log_print(ANDROID_LOG_DEBUG, "sqlcipher", fmt, elapsed, sqlite3_sql((sqlite3_stmt*)stmt)); +- } +-#endif ++// #ifdef __ANDROID__ ++// if(f == NULL) { ++// __android_log_print(ANDROID_LOG_DEBUG, "sqlcipher", fmt, elapsed, sqlite3_sql((sqlite3_stmt*)stmt)); ++// } ++// #endif + if(f) fprintf(f, fmt, elapsed, sqlite3_sql((sqlite3_stmt*)stmt)); + return SQLITE_OK; + } +@@ -104988,12 +104988,12 @@ void sqlcipher_log(unsigned int level, const char *message, ...) { + va_start(params, message); + + #ifdef CODEC_DEBUG +-#ifdef __ANDROID__ +- __android_log_vprint(ANDROID_LOG_DEBUG, "sqlcipher", message, params); +-#else ++// #ifdef __ANDROID__ ++// __android_log_vprint(ANDROID_LOG_DEBUG, "sqlcipher", message, params); ++// #else + vfprintf(stderr, message, params); + fprintf(stderr, "\n"); +-#endif ++// #endif + #endif + + if(level > sqlcipher_log_level || (sqlcipher_log_logcat == 0 && sqlcipher_log_file == NULL)) { +@@ -105026,11 +105026,11 @@ void sqlcipher_log(unsigned int level, const char *message, ...) { + fprintf((FILE*)sqlcipher_log_file, "\n"); + } + } +-#ifdef __ANDROID__ +- if(sqlcipher_log_logcat) { +- __android_log_vprint(ANDROID_LOG_DEBUG, "sqlcipher", message, params); +- } +-#endif ++// #ifdef __ANDROID__ ++// if(sqlcipher_log_logcat) { ++// __android_log_vprint(ANDROID_LOG_DEBUG, "sqlcipher", message, params); ++// } ++// #endif + end: + va_end(params); + } diff --git a/scripts/nix/direct-sqlcipher-android.patch b/scripts/nix/direct-sqlcipher-android.patch deleted file mode 100644 index 225172e4f2..0000000000 --- a/scripts/nix/direct-sqlcipher-android.patch +++ /dev/null @@ -1,324 +0,0 @@ -diff --git a/cbits/android/log.h b/cbits/android/log.h -new file mode 100644 -index 0000000..c8e715e ---- /dev/null -+++ b/cbits/android/log.h -@@ -0,0 +1,318 @@ -+/* -+ * Copyright (C) 2009 The Android Open Source Project -+ * -+ * Licensed under the Apache License, Version 2.0 (the "License"); -+ * you may not use this file except in compliance with the License. -+ * You may obtain a copy of the License at -+ * -+ * http://www.apache.org/licenses/LICENSE-2.0 -+ * -+ * Unless required by applicable law or agreed to in writing, software -+ * distributed under the License is distributed on an "AS IS" BASIS, -+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -+ * See the License for the specific language governing permissions and -+ * limitations under the License. -+ */ -+#pragma once -+/** -+ * @addtogroup Logging -+ * @{ -+ */ -+/** -+ * \file -+ * -+ * Support routines to send messages to the Android log buffer, -+ * which can later be accessed through the `logcat` utility. -+ * -+ * Each log message must have -+ * - a priority -+ * - a log tag -+ * - some text -+ * -+ * The tag normally corresponds to the component that emits the log message, -+ * and should be reasonably small. -+ * -+ * Log message text may be truncated to less than an implementation-specific -+ * limit (1023 bytes). -+ * -+ * Note that a newline character ("\n") will be appended automatically to your -+ * log message, if not already there. It is not possible to send several -+ * messages and have them appear on a single line in logcat. -+ * -+ * Please use logging in moderation: -+ * -+ * - Sending log messages eats CPU and slow down your application and the -+ * system. -+ * -+ * - The circular log buffer is pretty small, so sending many messages -+ * will hide other important log messages. -+ * -+ * - In release builds, only send log messages to account for exceptional -+ * conditions. -+ */ -+#include -+#include -+#include -+#include -+#if !defined(__BIONIC__) && !defined(__INTRODUCED_IN) -+#define __INTRODUCED_IN(x) -+#endif -+#ifdef __cplusplus -+extern "C" { -+#endif -+/** -+ * Android log priority values, in increasing order of priority. -+ */ -+typedef enum android_LogPriority { -+ /** For internal use only. */ -+ ANDROID_LOG_UNKNOWN = 0, -+ /** The default priority, for internal use only. */ -+ ANDROID_LOG_DEFAULT, /* only for SetMinPriority() */ -+ /** Verbose logging. Should typically be disabled for a release apk. */ -+ ANDROID_LOG_VERBOSE, -+ /** Debug logging. Should typically be disabled for a release apk. */ -+ ANDROID_LOG_DEBUG, -+ /** Informational logging. Should typically be disabled for a release apk. */ -+ ANDROID_LOG_INFO, -+ /** Warning logging. For use with recoverable failures. */ -+ ANDROID_LOG_WARN, -+ /** Error logging. For use with unrecoverable failures. */ -+ ANDROID_LOG_ERROR, -+ /** Fatal logging. For use when aborting. */ -+ ANDROID_LOG_FATAL, -+ /** For internal use only. */ -+ ANDROID_LOG_SILENT, /* only for SetMinPriority(); must be last */ -+} android_LogPriority; -+/** -+ * Writes the constant string `text` to the log, with priority `prio` and tag -+ * `tag`. -+ */ -+int __android_log_write(int prio, const char* tag, const char* text); -+/** -+ * Writes a formatted string to the log, with priority `prio` and tag `tag`. -+ * The details of formatting are the same as for -+ * [printf(3)](http://man7.org/linux/man-pages/man3/printf.3.html). -+ */ -+int __android_log_print(int prio, const char* tag, const char* fmt, ...) -+ __attribute__((__format__(printf, 3, 4))); -+/** -+ * Equivalent to `__android_log_print`, but taking a `va_list`. -+ * (If `__android_log_print` is like `printf`, this is like `vprintf`.) -+ */ -+int __android_log_vprint(int prio, const char* tag, const char* fmt, va_list ap) -+ __attribute__((__format__(printf, 3, 0))); -+/** -+ * Writes an assertion failure to the log (as `ANDROID_LOG_FATAL`) and to -+ * stderr, before calling -+ * [abort(3)](http://man7.org/linux/man-pages/man3/abort.3.html). -+ * -+ * If `fmt` is non-null, `cond` is unused. If `fmt` is null, the string -+ * `Assertion failed: %s` is used with `cond` as the string argument. -+ * If both `fmt` and `cond` are null, a default string is provided. -+ * -+ * Most callers should use -+ * [assert(3)](http://man7.org/linux/man-pages/man3/assert.3.html) from -+ * `<assert.h>` instead, or the `__assert` and `__assert2` functions -+ * provided by bionic if more control is needed. They support automatically -+ * including the source filename and line number more conveniently than this -+ * function. -+ */ -+void __android_log_assert(const char* cond, const char* tag, const char* fmt, ...) -+ __attribute__((__noreturn__)) __attribute__((__format__(printf, 3, 4))); -+/** -+ * Identifies a specific log buffer for __android_log_buf_write() -+ * and __android_log_buf_print(). -+ */ -+typedef enum log_id { -+ LOG_ID_MIN = 0, -+ /** The main log buffer. This is the only log buffer available to apps. */ -+ LOG_ID_MAIN = 0, -+ /** The radio log buffer. */ -+ LOG_ID_RADIO = 1, -+ /** The event log buffer. */ -+ LOG_ID_EVENTS = 2, -+ /** The system log buffer. */ -+ LOG_ID_SYSTEM = 3, -+ /** The crash log buffer. */ -+ LOG_ID_CRASH = 4, -+ /** The statistics log buffer. */ -+ LOG_ID_STATS = 5, -+ /** The security log buffer. */ -+ LOG_ID_SECURITY = 6, -+ /** The kernel log buffer. */ -+ LOG_ID_KERNEL = 7, -+ LOG_ID_MAX, -+ /** Let the logging function choose the best log target. */ -+ LOG_ID_DEFAULT = 0x7FFFFFFF -+} log_id_t; -+/** -+ * Writes the constant string `text` to the log buffer `id`, -+ * with priority `prio` and tag `tag`. -+ * -+ * Apps should use __android_log_write() instead. -+ */ -+int __android_log_buf_write(int bufID, int prio, const char* tag, const char* text); -+/** -+ * Writes a formatted string to log buffer `id`, -+ * with priority `prio` and tag `tag`. -+ * The details of formatting are the same as for -+ * [printf(3)](http://man7.org/linux/man-pages/man3/printf.3.html). -+ * -+ * Apps should use __android_log_print() instead. -+ */ -+int __android_log_buf_print(int bufID, int prio, const char* tag, const char* fmt, ...) -+ __attribute__((__format__(printf, 4, 5))); -+/** -+ * Logger data struct used for writing log messages to liblog via __android_log_write_logger_data() -+ * and sending log messages to user defined loggers specified in __android_log_set_logger(). -+ */ -+struct __android_log_message { -+ size_t -+ struct_size; /** Must be set to sizeof(__android_log_message) and is used for versioning. */ -+ int32_t buffer_id; /** {@link log_id_t} values. */ -+ int32_t priority; /** {@link android_LogPriority} values. */ -+ const char* tag; /** The tag for the log message. */ -+ const char* file; /** Optional file name, may be set to nullptr. */ -+ uint32_t line; /** Optional line number, ignore if file is nullptr. */ -+ const char* message; /** The log message itself. */ -+}; -+/** -+ * Prototype for the 'logger' function that is called for every log message. -+ */ -+typedef void (*__android_logger_function)(const struct __android_log_message* log_message); -+/** -+ * Prototype for the 'abort' function that is called when liblog will abort due to -+ * __android_log_assert() failures. -+ */ -+typedef void (*__android_aborter_function)(const char* abort_message); -+#if !defined(__ANDROID__) || __ANDROID_API__ >= 30 -+/** -+ * Writes the log message specified by log_message. log_message includes additional file name and -+ * line number information that a logger may use. log_message is versioned for backwards -+ * compatibility. -+ * This assumes that loggability has already been checked through __android_log_is_loggable(). -+ * Higher level logging libraries, such as libbase, first check loggability, then format their -+ * buffers, then pass the message to liblog via this function, and therefore we do not want to -+ * duplicate the loggability check here. -+ * -+ * @param log_message the log message itself, see {@link __android_log_message}. -+ * -+ * Available since API level 30. -+ */ -+void __android_log_write_log_message(struct __android_log_message* log_message) __INTRODUCED_IN(30); -+/** -+ * Sets a user defined logger function. All log messages sent to liblog will be set to the -+ * function pointer specified by logger for processing. It is not expected that log messages are -+ * already terminated with a new line. This function should add new lines if required for line -+ * separation. -+ * -+ * @param logger the new function that will handle log messages. -+ * -+ * Available since API level 30. -+ */ -+void __android_log_set_logger(__android_logger_function logger) __INTRODUCED_IN(30); -+/** -+ * Writes the log message to logd. This is an __android_logger_function and can be provided to -+ * __android_log_set_logger(). It is the default logger when running liblog on a device. -+ * -+ * @param log_message the log message to write, see {@link __android_log_message}. -+ * -+ * Available since API level 30. -+ */ -+void __android_log_logd_logger(const struct __android_log_message* log_message) __INTRODUCED_IN(30); -+/** -+ * Writes the log message to stderr. This is an __android_logger_function and can be provided to -+ * __android_log_set_logger(). It is the default logger when running liblog on host. -+ * -+ * @param log_message the log message to write, see {@link __android_log_message}. -+ * -+ * Available since API level 30. -+ */ -+void __android_log_stderr_logger(const struct __android_log_message* log_message) -+ __INTRODUCED_IN(30); -+/** -+ * Sets a user defined aborter function that is called for __android_log_assert() failures. This -+ * user defined aborter function is highly recommended to abort and be noreturn, but is not strictly -+ * required to. -+ * -+ * @param aborter the new aborter function, see {@link __android_aborter_function}. -+ * -+ * Available since API level 30. -+ */ -+void __android_log_set_aborter(__android_aborter_function aborter) __INTRODUCED_IN(30); -+/** -+ * Calls the stored aborter function. This allows for other logging libraries to use the same -+ * aborter function by calling this function in liblog. -+ * -+ * @param abort_message an additional message supplied when aborting, for example this is used to -+ * call android_set_abort_message() in __android_log_default_aborter(). -+ * -+ * Available since API level 30. -+ */ -+void __android_log_call_aborter(const char* abort_message) __INTRODUCED_IN(30); -+/** -+ * Sets android_set_abort_message() on device then aborts(). This is the default aborter. -+ * -+ * @param abort_message an additional message supplied when aborting. This functions calls -+ * android_set_abort_message() with its contents. -+ * -+ * Available since API level 30. -+ */ -+void __android_log_default_aborter(const char* abort_message) __attribute__((noreturn)) -+__INTRODUCED_IN(30); -+/** -+ * Use the per-tag properties "log.tag." along with the minimum priority from -+ * __android_log_set_minimum_priority() to determine if a log message with a given prio and tag will -+ * be printed. A non-zero result indicates yes, zero indicates false. -+ * -+ * If both a priority for a tag and a minimum priority are set by -+ * __android_log_set_minimum_priority(), then the lowest of the two values are to determine the -+ * minimum priority needed to log. If only one is set, then that value is used to determine the -+ * minimum priority needed. If none are set, then default_priority is used. -+ * -+ * @param prio the priority to test, takes {@link android_LogPriority} values. -+ * @param tag the tag to test. -+ * @param len the length of the tag. -+ * @param default_prio the default priority to use if no properties or minimum priority are set. -+ * @return an integer where 1 indicates that the message is loggable and 0 indicates that it is not. -+ * -+ * Available since API level 30. -+ */ -+int __android_log_is_loggable(int prio, const char* tag, int default_prio) __INTRODUCED_IN(30); -+int __android_log_is_loggable_len(int prio, const char* tag, size_t len, int default_prio) -+ __INTRODUCED_IN(30); -+/** -+ * Sets the minimum priority that will be logged for this process. -+ * -+ * @param priority the new minimum priority to set, takes @{link android_LogPriority} values. -+ * @return the previous set minimum priority as @{link android_LogPriority} values, or -+ * ANDROID_LOG_DEFAULT if none was set. -+ * -+ * Available since API level 30. -+ */ -+int32_t __android_log_set_minimum_priority(int32_t priority) __INTRODUCED_IN(30); -+/** -+ * Gets the minimum priority that will be logged for this process. If none has been set by a -+ * previous __android_log_set_minimum_priority() call, this returns ANDROID_LOG_DEFAULT. -+ * -+ * @return the current minimum priority as @{link android_LogPriority} values, or -+ * ANDROID_LOG_DEFAULT if none is set. -+ * -+ * Available since API level 30. -+ */ -+int32_t __android_log_get_minimum_priority(void) __INTRODUCED_IN(30); -+/** -+ * Sets the default tag if no tag is provided when writing a log message. Defaults to -+ * getprogname(). This truncates tag to the maximum log message size, though appropriate tags -+ * should be much smaller. -+ * -+ * @param tag the new log tag. -+ * -+ * Available since API level 30. -+ */ -+void __android_log_set_default_tag(const char* tag) __INTRODUCED_IN(30); -+#endif -+#ifdef __cplusplus -+} -+#endif -+/** @} */ From 7f9e68c58d889c7fed52a11329eae55a17e9feaa Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 13 Sep 2022 12:05:18 +0100 Subject: [PATCH 38/44] remove duplicate patch --- flake.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/flake.nix b/flake.nix index 007a9b117b..69ecf3e507 100644 --- a/flake.nix +++ b/flake.nix @@ -126,7 +126,6 @@ (androidPkgs.openssl.override { static = true; }) ]; packages.direct-sqlcipher.patches = [ - ./scripts/nix/direct-sqlcipher-2.3.27.patch ./scripts/nix/direct-sqlcipher-android-log.patch ]; }]; From 78f854e2c53940e6f135f8e9d8dbbf63bfa8ee4c Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:06:12 +0300 Subject: [PATCH 39/44] android: database encryption support with a passphrase (#1021) * Ability to encrypt credentials and to store them securelly * Don't regenerate key if it exists * Made code shorter * Refactoring * Initial support of encryped database * Changes in UI and notifications about database problems * Small changes to how we use chatController instance * Show unlock view in console automatically * Fixed wrong place of saving a key * Fixed a crash * update icons * Changing controller correctly * Enable migration * fix JNI * Fixed startup * Show database error view when password is wrong while enabling a chat * Chat controller re-init in one more place - also added one more alert * Scrollable columns and restarted service and worker * translations * database passphrase * update translations * translations Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> * update translations * update translations * update icon colors, show empty passphrase as not stored * update translation * update translations * shared section footer, bigger font, layout, change entropy bounds * correction Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> * update translations Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> --- apps/android/.gitignore | 1 + apps/android/app/src/main/cpp/simplex-api.c | 2 +- .../java/chat/simplex/app/MainActivity.kt | 14 +- .../main/java/chat/simplex/app/SimplexApp.kt | 44 +- .../java/chat/simplex/app/SimplexService.kt | 66 ++- .../java/chat/simplex/app/model/ChatModel.kt | 3 + .../java/chat/simplex/app/model/NtfManager.kt | 6 +- .../java/chat/simplex/app/model/SimpleXAPI.kt | 64 ++- .../java/chat/simplex/app/ui/theme/Color.kt | 1 + .../chat/simplex/app/views/TerminalView.kt | 74 ++- .../views/database/DatabaseEncryptionView.kt | 501 ++++++++++++++++++ .../app/views/database/DatabaseErrorView.kt | 175 ++++++ .../app/views/database/DatabaseView.kt | 118 ++++- .../app/views/helpers/DatabaseUtils.kt | 77 +++ .../views/helpers/MessagesFetcherWorker.kt | 13 +- .../chat/simplex/app/views/helpers/Section.kt | 5 +- .../chat/simplex/app/views/helpers/Util.kt | 5 + .../simplex/app/views/usersettings/Cryptor.kt | 53 ++ .../app/views/usersettings/SettingsView.kt | 19 +- .../app/src/main/res/values-ru/strings.xml | 70 ++- .../app/src/main/res/values/strings.xml | 64 ++- 21 files changed, 1278 insertions(+), 97 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Cryptor.kt diff --git a/apps/android/.gitignore b/apps/android/.gitignore index e4dd4a5169..644d967fb1 100644 --- a/apps/android/.gitignore +++ b/apps/android/.gitignore @@ -16,3 +16,4 @@ .externalNativeBuild .cxx local.properties +app/src/main/cpp/libs/ diff --git a/apps/android/app/src/main/cpp/simplex-api.c b/apps/android/app/src/main/cpp/simplex-api.c index 29676bd57e..10c9efd6fe 100644 --- a/apps/android/app/src/main/cpp/simplex-api.c +++ b/apps/android/app/src/main/cpp/simplex-api.c @@ -36,7 +36,7 @@ JNIEXPORT jstring JNICALL Java_chat_simplex_app_SimplexAppKt_chatMigrateDB(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey) { const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE); const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE); - jstring res = (jlong)chat_migrate_db(_dbPath, _dbKey); + jstring res = (*env)->NewStringUTF(env, chat_migrate_db(_dbPath, _dbKey)); (*env)->ReleaseStringUTFChars(env, dbPath, _dbPath); (*env)->ReleaseStringUTFChars(env, dbKey, _dbKey); return res; diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index de7b76f120..7d0e7ed911 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -32,6 +32,7 @@ import chat.simplex.app.views.call.IncomingCallAlertView import chat.simplex.app.views.chat.ChatView import chat.simplex.app.views.chatlist.ChatListView import chat.simplex.app.views.chatlist.openChat +import chat.simplex.app.views.database.DatabaseErrorView import chat.simplex.app.views.helpers.* import chat.simplex.app.views.newchat.connectViaUri import chat.simplex.app.views.newchat.withUriAction @@ -56,7 +57,6 @@ class MainActivity: FragmentActivity() { } } private val vm by viewModels() - private val chatController by lazy { (application as SimplexApp).chatController } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -251,6 +251,13 @@ fun MainPage( } chatsAccessAuthorized = userAuthorized.value == true } + var showChatDatabaseError by rememberSaveable { + mutableStateOf(chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null) + } + LaunchedEffect(chatModel.chatDbStatus.value) { + showChatDatabaseError = chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null + } + var showAdvertiseLAAlert by remember { mutableStateOf(false) } LaunchedEffect(showAdvertiseLAAlert) { if ( @@ -296,6 +303,11 @@ fun MainPage( val onboarding = chatModel.onboardingStage.value val userCreated = chatModel.userCreated.value when { + showChatDatabaseError -> { + chatModel.chatDbStatus.value?.let { + DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs) + } + } onboarding == null || userCreated == null -> SplashView() !chatsAccessAuthorized -> { if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index 3f0a5bc312..2cd3094951 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -35,14 +35,37 @@ external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String external fun chatParseMarkdown(str: String): String class SimplexApp: Application(), LifecycleEventObserver { - val chatController: ChatController by lazy { - val ctrl = chatInit(getFilesDirectory(applicationContext)) - ChatController(ctrl, ntfManager, applicationContext, appPreferences) + lateinit var chatController: ChatController + + fun initChatController(useKey: String? = null, startChat: Boolean = true) { + val dbKey = useKey ?: DatabaseUtils.getDatabaseKey() ?: "" + val res = DatabaseUtils.migrateChatDatabase(dbKey) + val ctrl = if (res.second is DBMigrationResult.OK) { + chatInitKey(getFilesDirectory(applicationContext), dbKey) + } else null + if (::chatController.isInitialized) { + chatController.ctrl = ctrl + } else { + chatController = ChatController(ctrl, ntfManager, applicationContext, appPreferences) + } + chatModel.chatDbEncrypted.value = res.first + chatModel.chatDbStatus.value = res.second + if (res.second != DBMigrationResult.OK) { + Log.d(TAG, "Unable to migrate successfully: ${res.second}") + } else if (startChat) { + withApi { + val user = chatController.apiGetActiveUser() + if (user == null) { + chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo + } else { + chatController.startChat(user) + } + } + } } - val chatModel: ChatModel by lazy { - chatController.chatModel - } + val chatModel: ChatModel + get() = chatController.chatModel private val ntfManager: NtfManager by lazy { NtfManager(applicationContext, appPreferences) @@ -55,15 +78,8 @@ class SimplexApp: Application(), LifecycleEventObserver { override fun onCreate() { super.onCreate() context = this + initChatController() ProcessLifecycleOwner.get().lifecycle.addObserver(this) - withApi { - val user = chatController.apiGetActiveUser() - if (user == null) { - chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo - } else { - chatController.startChat(user) - } - } } override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt index c1dc9a7af3..b567e0d8c7 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt @@ -9,7 +9,7 @@ import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.work.* -import chat.simplex.app.views.helpers.withApi +import chat.simplex.app.views.helpers.* import chat.simplex.app.views.onboarding.OnboardingStage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -24,7 +24,6 @@ class SimplexService: Service() { private var isStartingService = false private var notificationManager: NotificationManager? = null private var serviceNotification: Notification? = null - private val chatController by lazy { (application as SimplexApp).chatController } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.d(TAG, "onStartCommand startId: $startId") @@ -67,19 +66,21 @@ class SimplexService: Service() { val self = this isStartingService = true withApi { + val chatController = (application as SimplexApp).chatController try { - val user = chatController.apiGetActiveUser() - if (user == null) { - chatController.chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo - } else { - Log.w(TAG, "Starting foreground service") - chatController.startChat(user) - isServiceStarted = true - saveServiceState(self, ServiceState.STARTED) - wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run { - newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG).apply { - acquire() - } + Log.w(TAG, "Starting foreground service") + val chatDbStatus = chatController.chatModel.chatDbStatus.value + if (chatDbStatus != DBMigrationResult.OK) { + Log.w(chat.simplex.app.TAG, "SimplexService: problem with the database: $chatDbStatus") + showPassphraseNotification(chatDbStatus) + stopService() + return@withApi + } + isServiceStarted = true + saveServiceState(self, ServiceState.STARTED) + wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run { + newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG).apply { + acquire() } } } finally { @@ -227,6 +228,8 @@ class SimplexService: Service() { const val SERVICE_START_WORKER_INTERVAL_MINUTES = 3 * 60L const val SERVICE_START_WORKER_WORK_NAME_PERIODIC = "SimplexAutoRestartWorkerPeriodic" // Do not change! + private const val PASSPHRASE_NOTIFICATION_ID = 1535 + private const val WAKE_LOCK_TAG = "SimplexService::lock" private const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_SERVICE_PREFS" private const val SHARED_PREFS_SERVICE_STATE = "SIMPLEX_SERVICE_STATE" @@ -271,6 +274,41 @@ class SimplexService: Service() { return ServiceState.valueOf(value!!) } + fun showPassphraseNotification(chatDbStatus: DBMigrationResult?) { + val pendingIntent: PendingIntent = Intent(SimplexApp.context, MainActivity::class.java).let { notificationIntent -> + PendingIntent.getActivity(SimplexApp.context, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE) + } + + val title = when(chatDbStatus) { + is DBMigrationResult.ErrorNotADatabase -> generalGetString(R.string.enter_passphrase_notification_title) + is DBMigrationResult.OK -> return + else -> generalGetString(R.string.database_initialization_error_title) + } + + val description = when(chatDbStatus) { + is DBMigrationResult.ErrorNotADatabase -> generalGetString(R.string.enter_passphrase_notification_desc) + is DBMigrationResult.OK -> return + else -> generalGetString(R.string.database_initialization_error_desc) + } + + val builder = NotificationCompat.Builder(SimplexApp.context, NOTIFICATION_CHANNEL_ID) + .setSmallIcon(R.drawable.ntf_service_icon) + .setColor(0x88FFFF) + .setContentTitle(title) + .setContentText(description) + .setContentIntent(pendingIntent) + .setSilent(true) + .setShowWhen(false) + + val notificationManager = SimplexApp.context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(PASSPHRASE_NOTIFICATION_ID, builder.build()) + } + + fun cancelPassphraseNotification() { + val notificationManager = SimplexApp.context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(PASSPHRASE_NOTIFICATION_ID) + } + private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE) } } \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 60b5961c2c..c75149437c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.text.style.TextDecoration import chat.simplex.app.R import chat.simplex.app.ui.theme.* import chat.simplex.app.views.call.* +import chat.simplex.app.views.helpers.DBMigrationResult import chat.simplex.app.views.helpers.generalGetString import chat.simplex.app.views.onboarding.OnboardingStage import chat.simplex.app.views.usersettings.NotificationPreviewMode @@ -27,6 +28,8 @@ class ChatModel(val controller: ChatController) { val userCreated = mutableStateOf(null) val chatRunning = mutableStateOf(null) val chatDbChanged = mutableStateOf(false) + val chatDbEncrypted = mutableStateOf(false) + val chatDbStatus = mutableStateOf(null) val chats = mutableStateListOf() // current chat diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt index ca0aeb4732..070523353a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt @@ -34,13 +34,13 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference private val msgNtfTimeoutMs = 30000L init { - manager.createNotificationChannel(NotificationChannel(MessageChannel, "SimpleX Chat messages", NotificationManager.IMPORTANCE_HIGH)) - manager.createNotificationChannel(NotificationChannel(LockScreenCallChannel, "SimpleX Chat calls (lock screen)", NotificationManager.IMPORTANCE_HIGH)) + manager.createNotificationChannel(NotificationChannel(MessageChannel, generalGetString(R.string.ntf_channel_messages), NotificationManager.IMPORTANCE_HIGH)) + manager.createNotificationChannel(NotificationChannel(LockScreenCallChannel, generalGetString(R.string.ntf_channel_calls_lockscreen), NotificationManager.IMPORTANCE_HIGH)) manager.createNotificationChannel(callNotificationChannel()) } private fun callNotificationChannel(): NotificationChannel { - val callChannel = NotificationChannel(CallChannel, "SimpleX Chat calls", NotificationManager.IMPORTANCE_HIGH) + val callChannel = NotificationChannel(CallChannel, generalGetString(R.string.ntf_channel_calls), NotificationManager.IMPORTANCE_HIGH) val attrs = AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setUsage(AudioAttributes.USAGE_NOTIFICATION) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 10321ac912..d6996d8a26 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -106,6 +106,11 @@ class AppPreferences(val context: Context) { val networkTCPKeepCnt = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_CNT, KeepAliveOpts.defaults.keepCnt) val incognito = mkBoolPreference(SHARED_PREFS_INCOGNITO, false) + val storeDBPassphrase = mkBoolPreference(SHARED_PREFS_STORE_DB_PASSPHRASE, true) + val initialRandomDBPassphrase = mkBoolPreference(SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE, false) + val encryptedDBPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE, null) + val initializationVectorDBPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE, null) + val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name) val primaryColor = mkIntPreference(SHARED_PREFS_PRIMARY_COLOR, LightColorPalette.primary.toArgb()) @@ -180,6 +185,10 @@ class AppPreferences(val context: Context) { private const val SHARED_PREFS_NETWORK_TCP_KEEP_INTVL = "NetworkTCPKeepIntvl" private const val SHARED_PREFS_NETWORK_TCP_KEEP_CNT = "NetworkTCPKeepCnt" private const val SHARED_PREFS_INCOGNITO = "Incognito" + private const val SHARED_PREFS_STORE_DB_PASSPHRASE = "StoreDBPassphrase" + private const val SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE = "InitialRandomDBPassphrase" + private const val SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE = "EncryptedDBPassphrase" + private const val SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE = "InitializationVectorDBPassphrase" private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme" private const val SHARED_PREFS_PRIMARY_COLOR = "PrimaryColor" } @@ -187,7 +196,7 @@ class AppPreferences(val context: Context) { private const val MESSAGE_TIMEOUT: Int = 15_000_000 -open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager, val appContext: Context, val appPrefs: AppPreferences) { +open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val appContext: Context, val appPrefs: AppPreferences) { val chatModel = ChatModel(this) private var receiverStarted = false var lastMsgReceivedTimestamp: Long = System.currentTimeMillis() @@ -242,10 +251,12 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } suspend fun sendCmd(cmd: CC): CR { + val ctrl = ctrl ?: throw Exception("Controller is not initialized") + return withContext(Dispatchers.IO) { val c = cmd.cmdString if (cmd !is CC.ApiParseMarkdown) { - chatModel.terminalItems.add(TerminalItem.cmd(cmd)) + chatModel.terminalItems.add(TerminalItem.cmd(cmd.obfuscated)) Log.d(TAG, "sendCmd: ${cmd.cmdType}") } val json = chatSendCmd(ctrl, c) @@ -261,7 +272,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } } - private suspend fun recvMsg(): CR? { + private suspend fun recvMsg(ctrl: ChatCtrl): CR? { return withContext(Dispatchers.IO) { val json = chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT) if (json == "") { @@ -276,7 +287,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } private suspend fun recvMspLoop() { - val msg = recvMsg() + val msg = recvMsg(ctrl ?: return) if (msg != null) processReceivedMsg(msg) recvMspLoop() } @@ -343,6 +354,13 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager throw Error("failed to delete storage: ${r.responseType} ${r.details}") } + suspend fun apiStorageEncryption(currentKey: String = "", newKey: String = ""): CR.ChatCmdError? { + val r = sendCmd(CC.ApiStorageEncryption(DBEncryptionConfig(currentKey, newKey))) + if (r is CR.CmdOk) return null + else if (r is CR.ChatCmdError) return r + throw Exception("failed to set storage encryption: ${r.responseType} ${r.details}") + } + private suspend fun apiGetChats(): List { val r = sendCmd(CC.ApiGetChats()) if (r is CR.ApiChats ) return r.chats @@ -1197,6 +1215,7 @@ sealed class CC { class ApiExportArchive(val config: ArchiveConfig): CC() class ApiImportArchive(val config: ArchiveConfig): CC() class ApiDeleteStorage: CC() + class ApiStorageEncryption(val config: DBEncryptionConfig): CC() class ApiGetChats: CC() class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC() class ApiSendMessage(val type: ChatType, val id: Long, val file: String?, val quotedItemId: Long?, val mc: MsgContent): CC() @@ -1251,6 +1270,7 @@ sealed class CC { is ApiExportArchive -> "/_db export ${json.encodeToString(config)}" is ApiImportArchive -> "/_db import ${json.encodeToString(config)}" is ApiDeleteStorage -> "/_db delete" + is ApiStorageEncryption -> "/_db encryption ${json.encodeToString(config)}" is ApiGetChats -> "/_get chats pcc=on" is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search") is ApiSendMessage -> "/_send ${chatRef(type, id)} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}" @@ -1305,6 +1325,7 @@ sealed class CC { is ApiExportArchive -> "apiExportArchive" is ApiImportArchive -> "apiImportArchive" is ApiDeleteStorage -> "apiDeleteStorage" + is ApiStorageEncryption -> "apiStorageEncryption" is ApiGetChats -> "apiGetChats" is ApiGetChat -> "apiGetChat" is ApiSendMessage -> "apiSendMessage" @@ -1350,6 +1371,14 @@ sealed class CC { class ItemRange(val from: Long, val to: Long) + val obfuscated: CC + get() = when (this) { + is ApiStorageEncryption -> ApiStorageEncryption(DBEncryptionConfig(obfuscate(config.currentKey), obfuscate(config.newKey))) + else -> this + } + + private fun obfuscate(s: String): String = if (s.isEmpty()) "" else "***" + companion object { fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}" @@ -1381,6 +1410,9 @@ class ComposedMessage(val filePath: String?, val quotedItemId: Long?, val msgCon @Serializable class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = null, val parentTempDirectory: String? = null) +@Serializable +class DBEncryptionConfig(val currentKey: String, val newKey: String) + @Serializable data class NetCfg( val socksProxy: String? = null, @@ -1785,10 +1817,12 @@ sealed class ChatError { is ChatErrorChat -> "chat ${errorType.string}" is ChatErrorAgent -> "agent ${agentError.string}" is ChatErrorStore -> "store ${storeError.string}" + is ChatErrorDatabase -> "database ${databaseError.string}" } @Serializable @SerialName("error") class ChatErrorChat(val errorType: ChatErrorType): ChatError() @Serializable @SerialName("errorAgent") class ChatErrorAgent(val agentError: AgentErrorType): ChatError() @Serializable @SerialName("errorStore") class ChatErrorStore(val storeError: StoreError): ChatError() + @Serializable @SerialName("errorDatabase") class ChatErrorDatabase(val databaseError: DatabaseError): ChatError() } @Serializable @@ -1813,6 +1847,28 @@ sealed class StoreError { @Serializable @SerialName("groupNotFound") class GroupNotFound: StoreError() } +@Serializable +sealed class DatabaseError { + val string: String get() = when (this) { + is ErrorEncrypted -> "errorEncrypted" + is ErrorPlaintext -> "errorPlaintext" + is ErrorNoFile -> "errorPlaintext" + is ErrorExport -> "errorNoFile" + is ErrorOpen -> "errorExport" + } + @Serializable @SerialName("errorEncrypted") object ErrorEncrypted: DatabaseError() + @Serializable @SerialName("errorPlaintext") object ErrorPlaintext: DatabaseError() + @Serializable @SerialName("errorNoFile") class ErrorNoFile(val dbFile: String): DatabaseError() + @Serializable @SerialName("errorExport") class ErrorExport(val sqliteError: SQLiteError): DatabaseError() + @Serializable @SerialName("errorOpen") class ErrorOpen(val sqliteError: SQLiteError): DatabaseError() +} + +@Serializable +sealed class SQLiteError { + @Serializable @SerialName("errorNotADatabase") object ErrorNotADatabase: SQLiteError() + @Serializable @SerialName("error") class Error(val error: String): SQLiteError() +} + @Serializable sealed class AgentErrorType { val string: String get() = when (this) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt index 5b3e0b0ea6..8ac28570f5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt @@ -24,5 +24,6 @@ val GroupDark = Color(80, 80, 80, 60) val IncomingCallLight = Color(239, 237, 236, 255) val IncomingCallDark = Color(34, 30, 29, 255) val WarningOrange = Color(255, 127, 0, 255) +val WarningYellow = Color(255, 192, 0, 255) val FileLight = Color(183, 190, 199, 255) val FileDark = Color(101, 101, 106, 255) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index c0ca2ae479..f55b234571 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -1,5 +1,6 @@ package chat.simplex.app.views +import android.content.Context import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.foundation.* @@ -7,39 +8,86 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Lock import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.fragment.app.FragmentActivity +import chat.simplex.app.R import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.SimpleButton import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.* import chat.simplex.app.views.helpers.* import com.google.accompanist.insets.ProvideWindowInsets import com.google.accompanist.insets.navigationBarsWithImePadding -import kotlinx.coroutines.launch @Composable fun TerminalView(chatModel: ChatModel, close: () -> Unit) { val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) } BackHandler(onBack = close) - TerminalLayout( - chatModel.terminalItems, - composeState, - sendCommand = { - withApi { - // show "in progress" - chatModel.controller.sendCmd(CC.Console(composeState.value.message)) - composeState.value = ComposeState(useLinkPreviews = false) - // hide "in progress" + val authorized = remember { mutableStateOf(!chatModel.controller.appPrefs.performLA.get()) } + val context = LocalContext.current + LaunchedEffect(authorized.value) { + if (!authorized.value) { + runAuth(authorized = authorized, context) + } + } + if (authorized.value) { + TerminalLayout( + chatModel.terminalItems, + composeState, + sendCommand = { + withApi { + // show "in progress" + chatModel.controller.sendCmd(CC.Console(composeState.value.message)) + composeState.value = ComposeState(useLinkPreviews = false) + // hide "in progress" + } + }, + close + ) + } else { + Surface(Modifier.fillMaxSize()) { + Column(Modifier.background(MaterialTheme.colors.background)) { + CloseSheetBar(close) + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + SimpleButton( + stringResource(R.string.auth_unlock), + icon = Icons.Outlined.Lock, + click = { + runAuth(authorized = authorized, context) + } + ) + } } - }, - close + } + } +} + +private fun runAuth(authorized: MutableState, context: Context) { + authenticate( + generalGetString(R.string.auth_open_chat_console), + generalGetString(R.string.auth_log_in_using_credential), + context as FragmentActivity, + completed = { laResult -> + when (laResult) { + LAResult.Success, LAResult.Unavailable -> authorized.value = true + is LAResult.Error, LAResult.Failed -> authorized.value = false + } + } ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt new file mode 100644 index 0000000000..40726a4e55 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt @@ -0,0 +1,501 @@ +package chat.simplex.app.views.database + +import SectionItemView +import SectionItemViewSpaceBetween +import SectionTextFooter +import SectionView +import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.ZeroCornerSize +import androidx.compose.foundation.text.* +import androidx.compose.material.* +import androidx.compose.material.TextFieldDefaults.indicatorLine +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.outlined.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.* +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.* +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.app.R +import chat.simplex.app.SimplexApp +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.* +import chat.simplex.app.views.helpers.* +import kotlin.math.log2 + +@Composable +fun DatabaseEncryptionView(m: ChatModel) { + val progressIndicator = remember { mutableStateOf(false) } + val prefs = m.controller.appPrefs + val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } + val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) } + val storedKey = remember { val key = DatabaseUtils.getDatabaseKey(); mutableStateOf(key != null && key != "") } + // Do not do rememberSaveable on current key to prevent saving it on disk in clear text + val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.getDatabaseKey() ?: "" else "") } + val newKey = rememberSaveable { mutableStateOf("") } + val confirmNewKey = rememberSaveable { mutableStateOf("") } + + Box( + Modifier.fillMaxSize(), + ) { + DatabaseEncryptionLayout( + useKeychain, + prefs, + m.chatDbEncrypted.value, + currentKey, + newKey, + confirmNewKey, + storedKey, + initialRandomDBPassphrase, + onConfirmEncrypt = { + progressIndicator.value = true + withApi { + try { + val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value) + val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError + when { + sqliteError is SQLiteError.ErrorNotADatabase -> { + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg( + generalGetString(R.string.wrong_passphrase_title), + generalGetString(R.string.enter_correct_current_passphrase) + ) + } + } + error != null -> { + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(R.string.error_encrypting_database), + "failed to set storage encryption: ${error.responseType} ${error.details}" + ) + } + } + else -> { + prefs.initialRandomDBPassphrase.set(false) + initialRandomDBPassphrase.value = false + if (useKeychain.value) { + DatabaseUtils.setDatabaseKey(newKey.value) + } + resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value) + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(R.string.database_encrypted)) + } + } + } + } catch (e: Exception) { + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(R.string.error_encrypting_database), e.stackTraceToString()) + } + } + } + } + ) + if (progressIndicator.value) { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + Modifier + .padding(horizontal = 2.dp) + .size(30.dp), + color = HighOrLowlight, + strokeWidth = 2.5.dp + ) + } + } + } +} + +@Composable +fun DatabaseEncryptionLayout( + useKeychain: MutableState, + prefs: AppPreferences, + chatDbEncrypted: Boolean?, + currentKey: MutableState, + newKey: MutableState, + confirmNewKey: MutableState, + storedKey: MutableState, + initialRandomDBPassphrase: MutableState, + onConfirmEncrypt: () -> Unit, +) { + Column( + Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.Start, + ) { + Text( + stringResource(R.string.database_passphrase), + Modifier.padding(start = 16.dp, bottom = 24.dp), + style = MaterialTheme.typography.h1 + ) + + SectionView(null) { + SavePassphraseSetting(useKeychain.value, initialRandomDBPassphrase.value, storedKey.value) { checked -> + if (checked) { + setUseKeychain(true, useKeychain, prefs) + } else if (storedKey.value) { + AlertManager.shared.showAlertDialog( + title = generalGetString(R.string.remove_passphrase_from_keychain), + text = generalGetString(R.string.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(), + confirmText = generalGetString(R.string.remove_passphrase), + onConfirm = { + DatabaseUtils.removeDatabaseKey() + setUseKeychain(false, useKeychain, prefs) + storedKey.value = false + }, + destructive = true, + ) + } else { + setUseKeychain(false, useKeychain, prefs) + } + } + + if (!initialRandomDBPassphrase.value && chatDbEncrypted == true) { + DatabaseKeyField( + currentKey, + generalGetString(R.string.current_passphrase), + modifier = Modifier.padding(start = 8.dp), + isValid = ::validKey, + keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), + ) + } + + DatabaseKeyField( + newKey, + generalGetString(R.string.new_passphrase), + modifier = Modifier.padding(start = 8.dp), + showStrength = true, + isValid = ::validKey, + keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), + ) + val onClickUpdate = { + if (currentKey.value == "") { + if (useKeychain.value) + encryptDatabaseSavedAlert(onConfirmEncrypt) + else + encryptDatabaseAlert(onConfirmEncrypt) + } else { + if (useKeychain.value) + changeDatabaseKeySavedAlert(onConfirmEncrypt) + else + changeDatabaseKeyAlert(onConfirmEncrypt) + } + } + val disabled = currentKey.value == newKey.value || + newKey.value != confirmNewKey.value || + newKey.value.isEmpty() || + !validKey(currentKey.value) || + !validKey(newKey.value) + + DatabaseKeyField( + confirmNewKey, + generalGetString(R.string.confirm_new_passphrase), + modifier = Modifier.padding(start = 8.dp), + isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value }, + keyboardActions = KeyboardActions(onDone = { + if (!disabled) onClickUpdate() + defaultKeyboardAction(ImeAction.Done) + }), + ) + + SectionItemViewSpaceBetween(onClickUpdate, padding = PaddingValues(start = 8.dp, end = 12.dp), disabled = disabled) { + Text(generalGetString(R.string.update_database_passphrase), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary) + } + } + + Column { + if (chatDbEncrypted == false) { + SectionTextFooter(generalGetString(R.string.database_is_not_encrypted)) + } else if (useKeychain.value) { + if (storedKey.value) { + SectionTextFooter(generalGetString(R.string.keychain_is_storing_securely)) + if (initialRandomDBPassphrase.value) { + SectionTextFooter(generalGetString(R.string.encrypted_with_random_passphrase)) + } else { + SectionTextFooter(generalGetString(R.string.impossible_to_recover_passphrase)) + } + } else { + SectionTextFooter(generalGetString(R.string.keychain_allows_to_receive_ntfs)) + } + } else { + SectionTextFooter(generalGetString(R.string.you_have_to_enter_passphrase_every_time)) + SectionTextFooter(generalGetString(R.string.impossible_to_recover_passphrase)) + } + } + } +} + +fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(R.string.encrypt_database_question), + text = generalGetString(R.string.database_will_be_encrypted_and_passphrase_stored) + "\n" + storeSecurelySaved(), + confirmText = generalGetString(R.string.encrypt_database), + onConfirm = onConfirm, + destructive = false, + ) +} + +fun encryptDatabaseAlert(onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(R.string.encrypt_database_question), + text = generalGetString(R.string.database_will_be_encrypted) +"\n" + storeSecurelyDanger(), + confirmText = generalGetString(R.string.encrypt_database), + onConfirm = onConfirm, + destructive = true, + ) +} + +fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(R.string.change_database_passphrase_question), + text = generalGetString(R.string.database_encryption_will_be_updated) + "\n" + storeSecurelySaved(), + confirmText = generalGetString(R.string.update_database), + onConfirm = onConfirm, + destructive = false, + ) +} + +fun changeDatabaseKeyAlert(onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(R.string.change_database_passphrase_question), + text = generalGetString(R.string.database_passphrase_will_be_updated) + "\n" + storeSecurelyDanger(), + confirmText = generalGetString(R.string.update_database), + onConfirm = onConfirm, + destructive = true, + ) +} + +@Composable +fun SavePassphraseSetting( + useKeychain: Boolean, + initialRandomDBPassphrase: Boolean, + storedKey: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + SectionItemView() { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + if (storedKey) Icons.Filled.VpnKey else Icons.Filled.VpnKeyOff, + stringResource(R.string.save_passphrase_in_keychain), + tint = if (storedKey) SimplexGreen else HighOrLowlight + ) + Spacer(Modifier.padding(horizontal = 4.dp)) + Text( + stringResource(R.string.save_passphrase_in_keychain), + Modifier.padding(end = 24.dp), + color = Color.Unspecified + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + Switch( + checked = useKeychain, + onCheckedChange = onCheckedChange, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colors.primary, + uncheckedThumbColor = HighOrLowlight + ), + enabled = !initialRandomDBPassphrase + ) + } + } +} + +fun resetFormAfterEncryption( + m: ChatModel, + initialRandomDBPassphrase: MutableState, + currentKey: MutableState, + newKey: MutableState, + confirmNewKey: MutableState, + storedKey: MutableState, + stored: Boolean = false, +) { + m.chatDbEncrypted.value = true + initialRandomDBPassphrase.value = false + m.controller.appPrefs.initialRandomDBPassphrase.set(false) + currentKey.value = "" + newKey.value = "" + confirmNewKey.value = "" + storedKey.value = stored +} + +fun setUseKeychain(value: Boolean, useKeychain: MutableState, prefs: AppPreferences) { + useKeychain.value = value + prefs.storeDBPassphrase.set(value) +} + +fun storeSecurelySaved() = generalGetString(R.string.store_passphrase_securely) + +fun storeSecurelyDanger() = generalGetString(R.string.store_passphrase_securely_without_recover) + +private fun operationEnded(m: ChatModel, progressIndicator: MutableState, alert: () -> Unit) { + m.chatDbChanged.value = true + progressIndicator.value = false + alert.invoke() +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun DatabaseKeyField( + key: MutableState, + placeholder: String, + modifier: Modifier = Modifier, + showStrength: Boolean = false, + isValid: (String) -> Boolean, + keyboardActions: KeyboardActions = KeyboardActions(), +) { + var valid by remember { mutableStateOf(validKey(key.value)) } + var showKey by remember { mutableStateOf(false) } + val icon = if (valid) { + if (showKey) Icons.Filled.VisibilityOff else Icons.Filled.Visibility + } else Icons.Outlined.Error + val iconColor = if (valid) { + if (showStrength && key.value.isNotEmpty()) PassphraseStrength.check(key.value).color else HighOrLowlight + } else Color.Red + val keyboard = LocalSoftwareKeyboardController.current + val keyboardOptions = KeyboardOptions( + imeAction = if (keyboardActions.onNext != null) ImeAction.Next else ImeAction.Done, + autoCorrect = false, + keyboardType = KeyboardType.Password + ) + val state = remember { + mutableStateOf(TextFieldValue(key.value)) + } + val enabled = true + val colors = TextFieldDefaults.textFieldColors( + backgroundColor = Color.Unspecified, + textColor = MaterialTheme.colors.onBackground, + focusedIndicatorColor = Color.Unspecified, + unfocusedIndicatorColor = Color.Unspecified, + ) + val color = MaterialTheme.colors.onBackground + val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize) + val interactionSource = remember { MutableInteractionSource() } + BasicTextField( + value = state.value, + modifier = modifier + .fillMaxWidth() + .background(colors.backgroundColor(enabled).value, shape) + .indicatorLine(enabled, false, interactionSource, colors) + .defaultMinSize( + minWidth = TextFieldDefaults.MinWidth, + minHeight = TextFieldDefaults.MinHeight + ), + onValueChange = { + state.value = it + key.value = it.text + valid = isValid(it.text) + }, + cursorBrush = SolidColor(colors.cursorColor(false).value), + visualTransformation = if (showKey) + VisualTransformation.None + else + VisualTransformation { TransformedText(AnnotatedString(it.text.map { "*" }.joinToString(separator = "")), OffsetMapping.Identity) }, + keyboardOptions = keyboardOptions, + keyboardActions = KeyboardActions(onDone = { + keyboard?.hide() + keyboardActions.onDone?.invoke(this) + }), + singleLine = true, + textStyle = TextStyle.Default.copy( + color = color, + fontWeight = FontWeight.Normal, + fontSize = 16.sp + ), + interactionSource = interactionSource, + decorationBox = @Composable { innerTextField -> + TextFieldDefaults.TextFieldDecorationBox( + value = state.value.text, + innerTextField = innerTextField, + placeholder = { Text(placeholder, color = HighOrLowlight) }, + singleLine = true, + enabled = enabled, + isError = !valid, + trailingIcon = { + IconButton({ showKey = !showKey }) { + Icon(icon, null, tint = iconColor) + } + }, + interactionSource = interactionSource, + contentPadding = TextFieldDefaults.textFieldWithLabelPadding(start = 0.dp, end = 0.dp), + visualTransformation = VisualTransformation.None, + colors = colors + ) + } + ) +} + +// based on https://generatepasswords.org/how-to-calculate-entropy/ +private fun passphraseEntropy(s: String): Double { + var hasDigits = false + var hasUppercase = false + var hasLowercase = false + var hasSymbols = false + for (c in s) { + if (c.isDigit()) { + hasDigits = true + } else if (c.isLetter()) { + if (c.isUpperCase()) { + hasUppercase = true + } else { + hasLowercase = true + } + } else if (c.isASCII()) { + hasSymbols = true + } + } + val poolSize = (if (hasDigits) 10 else 0) + (if (hasUppercase) 26 else 0) + (if (hasLowercase) 26 else 0) + (if (hasSymbols) 32 else 0) + return s.length * log2(poolSize.toDouble()) +} + +private enum class PassphraseStrength(val color: Color) { + VERY_WEAK(Color.Red), WEAK(WarningOrange), REASONABLE(WarningYellow), STRONG(SimplexGreen); + + companion object { + fun check(s: String) = with(passphraseEntropy(s)) { + when { + this > 100 -> STRONG + this > 70 -> REASONABLE + this > 40 -> WEAK + else -> VERY_WEAK + } + } + } +} + +fun validKey(s: String): Boolean { + for (c in s) { + if (c.isWhitespace() || !c.isASCII()) { + return false + } + } + return true +} + +private fun Char.isASCII() = code in 32..126 + +@Preview +@Composable +fun PreviewDatabaseEncryptionLayout() { + SimpleXTheme { + DatabaseEncryptionLayout( + useKeychain = remember { mutableStateOf(true) }, + prefs = AppPreferences(SimplexApp.context), + chatDbEncrypted = true, + currentKey = remember { mutableStateOf("") }, + newKey = remember { mutableStateOf("") }, + confirmNewKey = remember { mutableStateOf("") }, + storedKey = remember { mutableStateOf(true) }, + initialRandomDBPassphrase = remember { mutableStateOf(true) }, + onConfirmEncrypt = {}, + ) + } +} \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt new file mode 100644 index 0000000000..13becac525 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt @@ -0,0 +1,175 @@ +package chat.simplex.app.views.database + +import SectionSpacer +import SectionView +import android.util.Log +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.simplex.app.* +import chat.simplex.app.R +import chat.simplex.app.model.AppPreferences +import chat.simplex.app.ui.theme.* +import chat.simplex.app.views.helpers.* +import chat.simplex.app.views.usersettings.NotificationsMode +import kotlinx.coroutines.* + +@Composable +fun DatabaseErrorView( + chatDbStatus: State, + appPreferences: AppPreferences, +) { + val dbKey = remember { mutableStateOf("") } + var storedDBKey by remember { mutableStateOf(DatabaseUtils.getDatabaseKey()) } + var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) } + val saveAndRunChatOnClick: () -> Unit = { + DatabaseUtils.setDatabaseKey(dbKey.value) + storedDBKey = dbKey.value + appPreferences.storeDBPassphrase.set(true) + useKeychain = true + appPreferences.initialRandomDBPassphrase.set(false) + runChat(dbKey.value, chatDbStatus, appPreferences) + } + val title = when (chatDbStatus.value) { + is DBMigrationResult.OK -> "" + is DBMigrationResult.ErrorNotADatabase -> if (useKeychain && !storedDBKey.isNullOrEmpty()) + generalGetString(R.string.wrong_passphrase) + else + generalGetString(R.string.encrypted_database) + is DBMigrationResult.Error -> generalGetString(R.string.database_error) + is DBMigrationResult.ErrorKeychain -> generalGetString(R.string.keychain_error) + is DBMigrationResult.Unknown -> generalGetString(R.string.database_error) + null -> "" // should never be here + } + + Column( + Modifier.fillMaxWidth().fillMaxHeight().verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Center, + ) { + Text( + title, + Modifier.padding(start = 16.dp, top = 16.dp, bottom = 24.dp), + style = MaterialTheme.typography.h1 + ) + SectionView(null) { + Column( + Modifier.padding(horizontal = 8.dp, vertical = 8.dp) + ) { + val buttonEnabled = validKey(dbKey.value) + when (val status = chatDbStatus.value) { + is DBMigrationResult.ErrorNotADatabase -> { + if (useKeychain && !storedDBKey.isNullOrEmpty()) { + Text(generalGetString(R.string.passphrase_is_different)) + DatabaseKeyField(dbKey, buttonEnabled) { + saveAndRunChatOnClick() + } + SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick) + SectionSpacer() + Text(String.format(generalGetString(R.string.file_with_path), status.dbFile)) + } else { + Text(generalGetString(R.string.database_passphrase_is_required)) + DatabaseKeyField(dbKey, buttonEnabled) { + if (useKeychain) saveAndRunChatOnClick() else runChat(dbKey.value, chatDbStatus, appPreferences) + } + if (useKeychain) { + SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick) + } else { + OpenChatButton(buttonEnabled) { runChat(dbKey.value, chatDbStatus, appPreferences) } + } + } + } + is DBMigrationResult.Error -> { + Text(String.format(generalGetString(R.string.file_with_path), status.dbFile)) + Text(String.format(generalGetString(R.string.error_with_info), status.migrationError)) + } + is DBMigrationResult.ErrorKeychain -> { + Text(generalGetString(R.string.cannot_access_keychain)) + } + is DBMigrationResult.Unknown -> { + Text(String.format(generalGetString(R.string.unknown_database_error_with_info), status.json)) + } + is DBMigrationResult.OK -> { + } + null -> { + } + } + } + } + } +} + +private fun runChat(dbKey: String, chatDbStatus: State, prefs: AppPreferences) { + try { + SimplexApp.context.initChatController(dbKey) + } catch (e: Exception) { + Log.d(TAG, "initializeChat ${e.stackTraceToString()}") + } + when (val status = chatDbStatus.value) { + is DBMigrationResult.OK -> { + SimplexService.cancelPassphraseNotification() + when (prefs.notificationsMode.get()) { + NotificationsMode.SERVICE.name -> CoroutineScope(Dispatchers.Default).launch { SimplexService.start(SimplexApp.context) } + NotificationsMode.PERIODIC.name -> SimplexApp.context.schedulePeriodicWakeUp() + } + } + is DBMigrationResult.ErrorNotADatabase -> { + AlertManager.shared.showAlertMsg( generalGetString(R.string.wrong_passphrase_title), generalGetString(R.string.enter_correct_passphrase)) + } + is DBMigrationResult.Error -> { + AlertManager.shared.showAlertMsg( generalGetString(R.string.database_error), status.migrationError) + } + is DBMigrationResult.ErrorKeychain -> { + AlertManager.shared.showAlertMsg( generalGetString(R.string.keychain_error)) + } + is DBMigrationResult.Unknown -> { + AlertManager.shared.showAlertMsg( generalGetString(R.string.unknown_error), status.json) + } + null -> {} + } +} + +@Composable +private fun DatabaseKeyField(text: MutableState, enabled: Boolean, onClick: (() -> Unit)? = null) { + DatabaseKeyField( + text, + generalGetString(R.string.enter_passphrase), + isValid = ::validKey, + keyboardActions = KeyboardActions(onDone = if (enabled) { + { onClick?.invoke() } + } else null + ) + ) +} + +@Composable +private fun ColumnScope.SaveAndOpenButton(enabled: Boolean, onClick: () -> Unit) { + TextButton(onClick, Modifier.align(Alignment.CenterHorizontally), enabled = enabled) { + Text(generalGetString(R.string.save_passphrase_and_open_chat)) + } +} + +@Composable +private fun ColumnScope.OpenChatButton(enabled: Boolean, onClick: () -> Unit) { + TextButton(onClick, Modifier.align(Alignment.CenterHorizontally), enabled = enabled) { + Text(generalGetString(R.string.open_chat)) + } +} + +@Preview +@Composable +fun PreviewChatInfoLayout() { + SimpleXTheme { + DatabaseErrorView( + remember { mutableStateOf(DBMigrationResult.ErrorNotADatabase("simplex_v1_chat.db")) }, + AppPreferences(SimplexApp.context) + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt index 11c9438313..b9c0c43995 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt @@ -15,10 +15,11 @@ import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material.icons.filled.Report +import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -28,13 +29,14 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.fragment.app.FragmentActivity import chat.simplex.app.* import chat.simplex.app.R import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.HighOrLowlight -import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* import chat.simplex.app.views.usersettings.* +import kotlinx.coroutines.* import kotlinx.datetime.* import java.io.* import java.text.SimpleDateFormat @@ -49,6 +51,7 @@ fun DatabaseView( val progressIndicator = remember { mutableStateOf(false) } val runChat = remember { mutableStateOf(m.chatRunning.value ?: true) } val prefs = m.controller.appPrefs + val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } val chatArchiveName = remember { mutableStateOf(prefs.chatArchiveName.get()) } val chatArchiveTime = remember { mutableStateOf(prefs.chatArchiveTime.get()) } val chatLastStart = remember { mutableStateOf(prefs.chatLastStart.get()) } @@ -68,12 +71,14 @@ fun DatabaseView( DatabaseLayout( progressIndicator.value, runChat.value, - m.chatDbChanged.value, + useKeychain.value, + m.chatDbEncrypted.value, + m.controller.appPrefs.initialRandomDBPassphrase, importArchiveLauncher, chatArchiveName, chatArchiveTime, chatLastStart, - startChat = { startChat(m, runChat, chatLastStart, context) }, + startChat = { startChat(m, runChat, chatLastStart, m.chatDbChanged) }, stopChatAlert = { stopChatAlert(m, runChat, context) }, exportArchive = { exportArchive(context, m, progressIndicator, chatArchiveName, chatArchiveTime, chatArchiveFile, saveArchiveLauncher) }, deleteChatAlert = { deleteChatAlert(m, progressIndicator) }, @@ -100,7 +105,9 @@ fun DatabaseView( fun DatabaseLayout( progressIndicator: Boolean, runChat: Boolean, - chatDbChanged: Boolean, + useKeyChain: Boolean, + chatDbEncrypted: Boolean?, + initialRandomDBPassphrase: Preference, importArchiveLauncher: ManagedActivityResultLauncher, chatArchiveName: MutableState, chatArchiveTime: MutableState, @@ -112,10 +119,10 @@ fun DatabaseLayout( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit) ) { val stopped = !runChat - val operationsDisabled = !stopped || progressIndicator || chatDbChanged + val operationsDisabled = !stopped || progressIndicator Column( - Modifier.fillMaxWidth(), + Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.Start, ) { Text( @@ -125,15 +132,30 @@ fun DatabaseLayout( ) SectionView(stringResource(R.string.run_chat_section)) { - RunChatSetting(runChat, stopped, chatDbChanged, startChat, stopChatAlert) + RunChatSetting(runChat, stopped, startChat, stopChatAlert) } SectionSpacer() SectionView(stringResource(R.string.chat_database_section)) { + val unencrypted = chatDbEncrypted == false + SettingsActionItem( + if (unencrypted) Icons.Outlined.LockOpen else if (useKeyChain) Icons.Filled.VpnKey else Icons.Outlined.Lock, + stringResource(R.string.database_passphrase), + click = showSettingsModal { DatabaseEncryptionView(it) }, + iconColor = if (unencrypted) WarningOrange else HighOrLowlight, + disabled = operationsDisabled + ) + SectionDivider() SettingsActionItem( Icons.Outlined.IosShare, stringResource(R.string.export_database), - exportArchive, + click = { + if (initialRandomDBPassphrase.get()) { + exportProhibitedAlert() + } else { + exportArchive() + } + }, textColor = MaterialTheme.colors.primary, disabled = operationsDisabled ) @@ -168,14 +190,10 @@ fun DatabaseLayout( ) } SectionTextFooter( - if (chatDbChanged) { - stringResource(R.string.restart_the_app_to_use_new_chat_database) + if (stopped) { + stringResource(R.string.you_must_use_the_most_recent_version_of_database) } else { - if (stopped) { - stringResource(R.string.you_must_use_the_most_recent_version_of_database) - } else { - stringResource(R.string.stop_chat_to_enable_database_actions) - } + stringResource(R.string.stop_chat_to_enable_database_actions) } ) } @@ -185,7 +203,6 @@ fun DatabaseLayout( fun RunChatSetting( runChat: Boolean, stopped: Boolean, - chatDbChanged: Boolean, startChat: () -> Unit, stopChatAlert: () -> Unit ) { @@ -195,13 +212,12 @@ fun RunChatSetting( Icon( if (stopped) Icons.Filled.Report else Icons.Filled.PlayArrow, chatRunningText, - tint = if (chatDbChanged) HighOrLowlight else if (stopped) Color.Red else MaterialTheme.colors.primary + tint = if (stopped) Color.Red else MaterialTheme.colors.primary ) Spacer(Modifier.padding(horizontal = 4.dp)) Text( chatRunningText, - Modifier.padding(end = 24.dp), - color = if (chatDbChanged) HighOrLowlight else Color.Unspecified + Modifier.padding(end = 24.dp) ) Spacer(Modifier.fillMaxWidth().weight(1f)) Switch( @@ -217,7 +233,6 @@ fun RunChatSetting( checkedThumbColor = MaterialTheme.colors.primary, uncheckedThumbColor = HighOrLowlight ), - enabled = !chatDbChanged ) } } @@ -228,16 +243,28 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String { return stringResource(if (chatArchiveTime < chatLastStart) R.string.old_database_archive else R.string.new_database_archive) } -private fun startChat(m: ChatModel, runChat: MutableState, chatLastStart: MutableState, context: Context) { +private fun startChat(m: ChatModel, runChat: MutableState, chatLastStart: MutableState, chatDbChanged: MutableState) { withApi { try { + if (chatDbChanged.value) { + SimplexApp.context.initChatController() + chatDbChanged.value = false + } + if (m.chatDbStatus.value !is DBMigrationResult.OK) { + /** Hide current view and show [DatabaseErrorView] */ + ModalManager.shared.closeModals() + return@withApi + } m.controller.apiStartChat() runChat.value = true m.chatRunning.value = true val ts = Clock.System.now() m.controller.appPrefs.chatLastStart.set(ts) chatLastStart.value = ts - SimplexService.start(context) + when (m.controller.appPrefs.notificationsMode.get()) { + NotificationsMode.SERVICE.name -> CoroutineScope(Dispatchers.Default).launch { SimplexService.start(SimplexApp.context) } + NotificationsMode.PERIODIC.name -> SimplexApp.context.schedulePeriodicWakeUp() + } } catch (e: Error) { runChat.value = false AlertManager.shared.showAlertMsg(generalGetString(R.string.error_starting_chat), e.toString()) @@ -250,11 +277,42 @@ private fun stopChatAlert(m: ChatModel, runChat: MutableState, context: title = generalGetString(R.string.stop_chat_question), text = generalGetString(R.string.stop_chat_to_export_import_or_delete_chat_database), confirmText = generalGetString(R.string.stop_chat_confirmation), - onConfirm = { stopChat(m, runChat, context) }, + onConfirm = { authStopChat(m, runChat, context) }, onDismiss = { runChat.value = true } ) } +private fun exportProhibitedAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(R.string.set_password_to_export), + text = generalGetString(R.string.set_password_to_export_desc), + ) +} + +private fun authStopChat(m: ChatModel, runChat: MutableState, context: Context) { + if (m.controller.appPrefs.performLA.get()) { + authenticate( + generalGetString(R.string.auth_stop_chat), + generalGetString(R.string.auth_log_in_using_credential), + context as FragmentActivity, + completed = { laResult -> + when (laResult) { + LAResult.Success, LAResult.Unavailable -> { + stopChat(m, runChat, context) + } + is LAResult.Error -> { + } + LAResult.Failed -> { + runChat.value = true + } + } + } + ) + } else { + stopChat(m, runChat, context) + } +} + private fun stopChat(m: ChatModel, runChat: MutableState, context: Context) { withApi { try { @@ -262,6 +320,7 @@ private fun stopChat(m: ChatModel, runChat: MutableState, context: Cont runChat.value = false m.chatRunning.value = false SimplexService.stop(context) + MessagesFetcherWorker.cancelAll() } catch (e: Error) { runChat.value = true AlertManager.shared.showAlertMsg(generalGetString(R.string.error_starting_chat), e.toString()) @@ -377,6 +436,7 @@ private fun importArchive(m: ChatModel, context: Context, importedArchiveUri: Ur try { val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString()) m.controller.apiImportArchive(config) + DatabaseUtils.removeDatabaseKey() operationEnded(m, progressIndicator) { AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), generalGetString(R.string.restart_the_app_to_use_imported_chat_database)) } @@ -429,6 +489,8 @@ private fun deleteChat(m: ChatModel, progressIndicator: MutableState) { withApi { try { m.controller.apiDeleteStorage() + DatabaseUtils.removeDatabaseKey() + m.controller.appPrefs.storeDBPassphrase.set(true) operationEnded(m, progressIndicator) { AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_deleted), generalGetString(R.string.restart_the_app_to_create_a_new_chat_profile)) } @@ -458,7 +520,9 @@ fun PreviewDatabaseLayout() { DatabaseLayout( progressIndicator = false, runChat = true, - chatDbChanged = false, + useKeyChain = false, + chatDbEncrypted = false, + initialRandomDBPassphrase = Preference({ true }, {}), importArchiveLauncher = rememberGetContentLauncher {}, chatArchiveName = remember { mutableStateOf("dummy_archive") }, chatArchiveTime = remember { mutableStateOf(Clock.System.now()) }, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt new file mode 100644 index 0000000000..431c5e177a --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt @@ -0,0 +1,77 @@ +package chat.simplex.app.views.helpers + +import android.util.Log +import chat.simplex.app.* +import chat.simplex.app.model.AppPreferences +import chat.simplex.app.model.json +import chat.simplex.app.views.usersettings.Cryptor +import kotlinx.serialization.* +import java.io.File +import java.security.SecureRandom + +object DatabaseUtils { + private val cryptor = Cryptor() + + private val appPreferences: AppPreferences by lazy { + AppPreferences(SimplexApp.context) + } + + private const val DATABASE_PASSWORD_ALIAS: String = "databasePassword" + + fun hasDatabase(filesDirectory: String): Boolean = File(filesDirectory + File.separator + "files_chat.db").exists() + + fun getDatabaseKey(): String? { + return cryptor.decryptData( + appPreferences.encryptedDBPassphrase.get()?.toByteArrayFromBase64() ?: return null, + appPreferences.initializationVectorDBPassphrase.get()?.toByteArrayFromBase64() ?: return null, + DATABASE_PASSWORD_ALIAS, + ) + } + + fun setDatabaseKey(key: String) { + val data = cryptor.encryptText(key, DATABASE_PASSWORD_ALIAS) + appPreferences.encryptedDBPassphrase.set(data.first.toBase64String()) + appPreferences.initializationVectorDBPassphrase.set(data.second.toBase64String()) + } + + fun removeDatabaseKey() { + cryptor.deleteKey(DATABASE_PASSWORD_ALIAS) + appPreferences.encryptedDBPassphrase.set(null) + appPreferences.initializationVectorDBPassphrase.set(null) + } + + fun migrateChatDatabase(useKey: String? = null): Pair { + Log.d(TAG, "migrateChatDatabase ${appPreferences.storeDBPassphrase.get()}") + val dbPath = getFilesDirectory(SimplexApp.context) + var dbKey = "" + val useKeychain = appPreferences.storeDBPassphrase.get() + if (useKey != null) { + dbKey = useKey + } else if (useKeychain) { + if (!hasDatabase(dbPath)) { + dbKey = randomDatabasePassword() + appPreferences.initialRandomDBPassphrase.set(true) + } else { + dbKey = getDatabaseKey() ?: "" + } + } + Log.d(TAG, "migrateChatDatabase DB path: $dbPath") + val migrated = chatMigrateDB(dbPath, dbKey) + val res: DBMigrationResult = kotlin.runCatching { + json.decodeFromString(migrated) + }.getOrElse { DBMigrationResult.Unknown(migrated) } + val encrypted = dbKey != "" + return encrypted to res + } + + private fun randomDatabasePassword(): String = ByteArray(32).apply { SecureRandom().nextBytes(this) }.toBase64String() +} + +@Serializable +sealed class DBMigrationResult { + @Serializable @SerialName("ok") object OK: DBMigrationResult() + @Serializable @SerialName("errorNotADatabase") class ErrorNotADatabase(val dbFile: String): DBMigrationResult() + @Serializable @SerialName("error") class Error(val dbFile: String, val migrationError: String): DBMigrationResult() + @Serializable @SerialName("errorKeychain") object ErrorKeychain: DBMigrationResult() + @Serializable @SerialName("unknown") class Unknown(val json: String): DBMigrationResult() +} \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/MessagesFetcherWorker.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/MessagesFetcherWorker.kt index aff69665ad..17b856017d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/MessagesFetcherWorker.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/MessagesFetcherWorker.kt @@ -4,6 +4,7 @@ import android.content.Context import android.util.Log import androidx.work.* import chat.simplex.app.* +import chat.simplex.app.SimplexService.Companion.showPassphraseNotification import kotlinx.coroutines.* import java.util.Date import java.util.concurrent.TimeUnit @@ -51,12 +52,18 @@ class MessagesFetcherWork( return Result.success() } val durationSeconds = inputData.getInt(INPUT_DATA_DURATION, 60) + var shouldReschedule = true try { withTimeout(durationSeconds * 1000L) { val chatController = (applicationContext as SimplexApp).chatController - val user = chatController.apiGetActiveUser() ?: return@withTimeout + val chatDbStatus = chatController.chatModel.chatDbStatus.value + if (chatDbStatus != DBMigrationResult.OK) { + Log.w(TAG, "Worker: problem with the database: $chatDbStatus") + showPassphraseNotification(chatDbStatus) + shouldReschedule = false + return@withTimeout + } Log.w(TAG, "Worker: starting work") - chatController.startChat(user) // Give some time to start receiving messages delay(10_000) while (!isStopped) { @@ -75,7 +82,7 @@ class MessagesFetcherWork( Log.d(TAG, "Worker: unexpected exception: ${e.stackTraceToString()}") } - reschedule() + if (shouldReschedule) reschedule() return Result.success() } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Section.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Section.kt index bc653fa2b3..bb03fa565d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Section.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Section.kt @@ -135,9 +135,10 @@ fun SectionItemWithValue( fun SectionTextFooter(text: String) { Text( text, - Modifier.padding(horizontal = 16.dp).padding(top = 5.dp).fillMaxWidth(0.9F), + Modifier.padding(horizontal = 16.dp).padding(top = 8.dp).fillMaxWidth(0.9F), color = HighOrLowlight, - fontSize = 12.sp + lineHeight = 18.sp, + fontSize = 14.sp ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt index d09fcc4ce1..12af59fc89 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -10,6 +10,7 @@ import android.provider.OpenableColumns import android.text.Spanned import android.text.SpannedString import android.text.style.* +import android.util.Base64 import android.util.Log import android.view.ViewTreeObserver import androidx.annotation.StringRes @@ -403,3 +404,7 @@ fun removeFile(context: Context, fileName: String): Boolean { } return fileDeleted } + +fun ByteArray.toBase64String() = Base64.encodeToString(this, Base64.DEFAULT) + +fun String.toByteArrayFromBase64() = Base64.decode(this, Base64.DEFAULT) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Cryptor.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Cryptor.kt new file mode 100644 index 0000000000..560587ff66 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Cryptor.kt @@ -0,0 +1,53 @@ +package chat.simplex.app.views.usersettings + +import android.annotation.SuppressLint +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import java.security.KeyStore +import javax.crypto.* +import javax.crypto.spec.GCMParameterSpec + +@SuppressLint("ObsoleteSdkInt") +internal class Cryptor { + private var keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + + fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String { + val cipher: Cipher = Cipher.getInstance(TRANSFORMATION) + val spec = GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, getSecretKey(alias), spec) + return String(cipher.doFinal(data)) + } + + fun encryptText(text: String, alias: String): Pair { + val cipher: Cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.ENCRYPT_MODE, createSecretKey(alias)) + return Pair(cipher.doFinal(text.toByteArray(charset("UTF-8"))), cipher.iv) + } + + fun deleteKey(alias: String) { + if (!keyStore.containsAlias(alias)) return + keyStore.deleteEntry(alias) + } + + private fun createSecretKey(alias: String): SecretKey { + if (keyStore.containsAlias(alias)) return getSecretKey(alias) + val keyGenerator: KeyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM, "AndroidKeyStore") + keyGenerator.init( + KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(BLOCK_MODE) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build() + ) + return keyGenerator.generateKey() + } + + private fun getSecretKey(alias: String): SecretKey { + return (keyStore.getEntry(alias, null) as KeyStore.SecretKeyEntry).secretKey + } + + companion object { + private val KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES + private val BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM + private val TRANSFORMATION = "AES/GCM/NoPadding" + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index d74d2e9477..fd1e150a91 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -44,6 +44,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { SettingsLayout( profile = user.profile, stopped, + chatModel.chatDbEncrypted.value == true, chatModel.incognito, chatModel.controller.appPrefs.incognito, developerTools = chatModel.controller.appPrefs.developerTools, @@ -79,6 +80,7 @@ val simplexTeamUri = fun SettingsLayout( profile: LocalProfile, stopped: Boolean, + encrypted: Boolean, incognito: MutableState, incognitoPref: Preference, developerTools: Preference, @@ -113,7 +115,7 @@ fun SettingsLayout( SectionDivider() SettingsActionItem(Icons.Outlined.QrCode, stringResource(R.string.your_simplex_contact_address), showModal { UserAddressView(it) }, disabled = stopped) SectionDivider() - DatabaseItem(showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) + DatabaseItem(encrypted, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) } SectionSpacer() @@ -199,7 +201,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) { } } -@Composable private fun DatabaseItem(openDatabaseView: () -> Unit, stopped: Boolean) { +@Composable private fun DatabaseItem(encrypted: Boolean, openDatabaseView: () -> Unit, stopped: Boolean) { SectionItemView(openDatabaseView) { Row( Modifier.fillMaxWidth(), @@ -207,12 +209,12 @@ fun MaintainIncognitoState(chatModel: ChatModel) { ) { Row { Icon( - Icons.Outlined.Archive, - contentDescription = stringResource(R.string.database_export_and_import), - tint = HighOrLowlight, + Icons.Outlined.FolderOpen, + contentDescription = stringResource(R.string.database_passphrase_and_export), + tint = if (encrypted) HighOrLowlight else WarningOrange, ) Spacer(Modifier.padding(horizontal = 4.dp)) - Text(stringResource(R.string.database_export_and_import)) + Text(stringResource(R.string.database_passphrase_and_export)) } if (stopped) { Icon( @@ -305,9 +307,9 @@ fun MaintainIncognitoState(chatModel: ChatModel) { } @Composable -fun SettingsActionItem(icon: ImageVector, text: String, click: (() -> Unit)? = null, textColor: Color = Color.Unspecified, disabled: Boolean = false) { +fun SettingsActionItem(icon: ImageVector, text: String, click: (() -> Unit)? = null, textColor: Color = Color.Unspecified, iconColor: Color = HighOrLowlight, disabled: Boolean = false) { SectionItemView(click, disabled = disabled) { - Icon(icon, text, tint = HighOrLowlight) + Icon(icon, text, tint = iconColor) Spacer(Modifier.padding(horizontal = 4.dp)) Text(text, color = if (disabled) HighOrLowlight else textColor) } @@ -355,6 +357,7 @@ fun PreviewSettingsLayout() { SettingsLayout( profile = LocalProfile.sampleData, stopped = false, + encrypted = false, incognito = remember { mutableStateOf(false) }, incognitoPref = Preference({ false}, {}), developerTools = Preference({ false }, {}), diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index daa5ebc3ce..bd8111e434 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -67,12 +67,21 @@ Периодические уведомления Периодические уведомления выключены! Приложение периодически получает новые сообщения — это потребляет несколько процентов батареи в день. Приложение не использует push уведомления — данные не отправляются с вашего устройства на сервер. + Введите пароль + Для получения уведомлений, пожалуйста, введите пароль от базы данных + Ошибка данных + Ошибка при инициализации данных. Нажмите чтобы узнать больше SimpleX Chat сервис Приём сообщений… Скрыть + + SimpleX Chat сообщения + SimpleX Chat звонки + SimpleX Chat звонки (экран блокировки) + Сервис уведомлений Показывать уведомления @@ -112,6 +121,8 @@ Аутентификация устройства не включена. Вы можете включить блокировку SimpleX в Настройках после включения аутентификации. Аутентификация устройства выключена. Отключение блокировки SimpleX Chat. Повторить + Остановить чат + Открыть консоль Ответить @@ -291,7 +302,7 @@ Настройки Ваш SimpleX адрес - Экспорт и импорт архива чата + Пароль и экспорт базы Информация о SimpleX Chat Как использовать Форматирование сообщений @@ -512,11 +523,12 @@ Режим Инкогнито - Данные чата + База данных ЗАПУСТИТЬ ЧАТ Чат запущен Чат остановлен - АРХИВ ЧАТА + БАЗА ДАННЫХ + Пароль базы данных Экспорт архива чата Импорт архива чата Новый архив чата @@ -524,8 +536,10 @@ Удалить данные чата Ошибка при запуске чата Остановить чат? - Остановите чат, чтобы экспортировать или импортировать архив чата или удалить данные чата. Вы не сможете получать и отправлять сообщения, пока чат остановлен. + Остановите чат, чтобы экспортировать или импортировать архив чата или удалить базу данных. Вы не сможете получать и отправлять сообщения, пока чат остановлен. Остановить + Установите пароль + База данных зашифрована случайным паролем. Пожалуйста, поменяйте его перед экспортом. Ошибка при остановке чата Ошибка при экспорте архива чата Импортировать архив чата? @@ -541,7 +555,53 @@ Перезапустите приложение, чтобы создать новый профиль. Используйте самую последнюю версию архива чата и ТОЛЬКО на одном устройстве, иначе вы можете перестать получать сообщения от некоторых контактов. Остановите чат, чтобы разблокировать операции с архивом чата. - Перезапустите приложение, чтобы использовать новый архив чата. + + + Сохранить пароль в Keystore + База данных зашифрована! + Ошибка при шифровании + Удалить пароль из Keystore? + Уведомления будут работать только до остановки приложения! + Удалить + Зашифровать + Поменять + Текущий пароль… + Новый пароль… + Подтвердите новый пароль… + Поменять пароль + Пожалуйста, введите правильный пароль. + База данных НЕ зашифрована. Установите пароль, чтобы защитить ваши данные. + Android Keystore используется для безопасного хранения пароля - это позволяет стабильно получать уведомления в фоновом режиме. + База данных зашифрована случайным паролем, вы можете его поменять. + Внимание: вы не сможете восстановить или поменять пароль, если вы его потеряете. + Пароль базы данных будет безопасно сохранен в Android Keystore после запуска чата или изменения пароля - это позволит стабильно получать уведомления. + Пароль не сохранен на устройстве — вы будете должны ввести его при каждом запуске чата. + Зашифровать базу данных? + Поменять пароль базы данных? + База данных будет зашифрована. + База данных будут зашифрована и пароль сохранен в Keystore. + Пароль базы данных будет изменен и сохранен в Keystore. + Пароль базы данных будет изменен. + Пожалуйста, надежно сохраните пароль, вы НЕ сможете его поменять, если потеряете. + Пожалуйста, надежно сохраните пароль, вы НЕ сможете открыть чат, если вы потеряете пароль. + + + Неправильный пароль базы данных + База данных зашифрована + Ошибка базы данных + Ошибка Keystore + Пароль базы данных отличается от пароля сохрененного в Keystore. + Файл: %s + Введите пароль базы данных чтобы открыть чат. + Ошибка: %s + Невозможно сохранить пароль в Keystore + Неизвестная ошибка базы данных: %s + Неправильный пароль! + Введите правильный пароль. + Неизвестная ошибка + Введите пароль… + Сохранить пароль и открыть чат + Открыть чат Чат остановлен diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 032eb9fa6f..590288ca1e 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -67,12 +67,21 @@ Periodic notifications Periodic notifications are disabled! The app fetches new messages periodically — it uses a few percent of the battery per day. The app doesn\'t use push notifications — data from your device is not sent to the servers. + Passphrase is needed + To receive notifications, please, enter the database passphrase + Can\'t initialize the database + The database is not working correctly. Tap to learn more SimpleX Chat service Receiving messages… Hide + + SimpleX Chat messages + SimpleX Chat calls + SimpleX Chat calls (lock screen) + Notification service Show preview @@ -112,6 +121,8 @@ Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication. Device authentication is disabled. Turning off SimpleX Lock. Retry + Stop chat + Open chat console Reply @@ -295,7 +306,7 @@ Your settings Your SimpleX contact address - Database export & import + Database passphrase & export About SimpleX Chat How to use it Markdown help @@ -518,6 +529,7 @@ Chat is running Chat is stopped CHAT DATABASE + Database passphrase Export database Import database New database archive @@ -527,6 +539,8 @@ Stop chat? Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. Stop + Set passphrase to export + Database is encrypted using a random passphrase. Please change it before exporting. Error stopping chat Error exporting chat database Import chat database? @@ -542,7 +556,53 @@ Restart the app to create a new chat profile. You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. Stop chat to enable database actions. - Restart the app to use new chat database. + + + Save passphrase in Keystore + Database encrypted! + Error encrypting database + Remove passphrase from Keystore? + Notifications will be delivered only until the app stops! + Remove + Encrypt + Update + Current passphrase… + New passphrase… + Confirm new passphrase… + Update database passphrase + Please enter correct current passphrase. + Your chat database is not encrypted - set passphrase to protect it. + Android Keystore is used to securely store passphrase - it allows notification service to work. + Database is encrypted using a random passphrase, you can change it. + Please note: you will NOT be able to recover or change passphrase if you lose it. + Android Keystore will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving notifications. + You have to enter passphrase every time the app starts - it is not stored on the device. + Encrypt database? + Change database passphrase? + Database will be encrypted. + Database will be encrypted and the passphrase stored in the Keystore. + Database encryption passphrase will be updated and stored in the Keystore. + Database encryption passphrase will be updated. + Please store passphrase securely, you will NOT be able to change it if you lose it. + Please store passphrase securely, you will NOT be able to access chat if you lose it. + + + Wrong database passphrase + Encrypted database + Database error + Keychain error + Database passphrase is different from saved in the Keystore. + File: %s + Database passphrase is required to open chat. + Error: %s + Cannot access Keystore to save database password + Unknown database error: %s + Wrong passphrase! + Enter correct passphrase. + Unknown error + Enter passphrase… + Save passphrase and open chat + Open chat Chat is stopped From 76a7dfeabb1f8c11bed18367d94cf3c6dcdbd95c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:04:41 +0100 Subject: [PATCH 40/44] ios: localize database encryption (#1048) * ios: localize database encryption * fix incorrect language in NSE localizations * corrections Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> * translations Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> --- .../app/src/main/res/values-ru/strings.xml | 2 +- .../Database/DatabaseEncryptionView.swift | 2 +- .../en.xcloc/Localized Contents/en.xliff | 280 ++++++++++++++++- .../SimpleX NSE/en.lproj/Localizable.strings | 1 + .../en.lproj/Localizable.strings | 3 + .../ru.xcloc/Localized Contents/ru.xliff | 282 +++++++++++++++++- .../SimpleX NSE/en.lproj/Localizable.strings | 1 + .../en.lproj/Localizable.strings | 3 + .../SimpleX NSE/en.lproj/Localizable.strings | 1 + .../SimpleX NSE/ru.lproj/Localizable.strings | 2 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 2 + apps/ios/SimpleXChat/ChatTypes.swift | 2 +- apps/ios/SimpleXChat/Notifications.swift | 2 +- apps/ios/en.lproj/Localizable.strings | 3 + apps/ios/ru.lproj/Localizable.strings | 162 +++++++++- 15 files changed, 729 insertions(+), 19 deletions(-) create mode 100644 apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings create mode 100644 apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings create mode 100644 apps/ios/SimpleX NSE/en.lproj/Localizable.strings diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index bd8111e434..a9ae7ff7e3 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -579,7 +579,7 @@ Зашифровать базу данных? Поменять пароль базы данных? База данных будет зашифрована. - База данных будут зашифрована и пароль сохранен в Keystore. + База данных будет зашифрована и пароль сохранен в Keystore. Пароль базы данных будет изменен и сохранен в Keystore. Пароль базы данных будет изменен. Пожалуйста, надежно сохраните пароль, вы НЕ сможете его поменять, если потеряете. diff --git a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift index 7358d9a45b..3423f8db9b 100644 --- a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift @@ -223,7 +223,7 @@ struct DatabaseEncryptionView: View { return Alert(title: Text("Database encrypted!")) case .currentPassphraseError: return Alert( - title: Text("Wrong passsphrase!"), + title: Text("Wrong passphrase!"), message: Text("Please enter correct current passphrase.") ) case let .error(title, error): diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 3dba7fc0b4..4baf962d3c 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -127,6 +127,11 @@ **Paste received link** or open it in the browser and tap **Open in mobile app**. No comment provided by engineer. + + **Please note**: you will NOT be able to recover or change passphrase if you lose it. + **Please note**: you will NOT be able to recover or change passphrase if you lose it. + No comment provided by engineer. + **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. @@ -137,6 +142,11 @@ **Scan QR code**: to connect to your contact in person or via video call. No comment provided by engineer. + + **Warning**: Instant push notifications require passphrase saved in Keychain. + **Warning**: Instant push notifications require passphrase saved in Keychain. + No comment provided by engineer. + **e2e encrypted** audio call **e2e encrypted** audio call @@ -303,6 +313,16 @@ Cancel No comment provided by engineer. + + Cannot access keychain to save database password + Cannot access keychain to save database password + No comment provided by engineer. + + + Change database passphrase? + Change database passphrase? + No comment provided by engineer. + Chat archive Chat archive @@ -346,7 +366,7 @@ Chats Chats - back button to return to chats list + No comment provided by engineer. Choose file @@ -388,6 +408,11 @@ Confirm No comment provided by engineer. + + Confirm new passphrase… + Confirm new passphrase… + No comment provided by engineer. + Connect Connect @@ -528,6 +553,11 @@ Created on %@ No comment provided by engineer. + + Current passphrase… + Current passphrase… + No comment provided by engineer. + Currently maximum supported file size is %@. Currently maximum supported file size is %@. @@ -543,9 +573,72 @@ Database ID No comment provided by engineer. - - Database export & import - Database export & import + + Database encrypted! + Database encrypted! + No comment provided by engineer. + + + Database encryption passphrase will be updated and stored in the keychain. + + Database encryption passphrase will be updated and stored in the keychain. + + No comment provided by engineer. + + + Database encryption passphrase will be updated. + + Database encryption passphrase will be updated. + + No comment provided by engineer. + + + Database error + Database error + No comment provided by engineer. + + + Database is encrypted using a random passphrase, you can change it. + Database is encrypted using a random passphrase, you can change it. + No comment provided by engineer. + + + Database is encrypted using a random passphrase. Please change it before exporting. + Database is encrypted using a random passphrase. Please change it before exporting. + No comment provided by engineer. + + + Database passphrase + Database passphrase + No comment provided by engineer. + + + Database passphrase & export + Database passphrase & export + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + Database passphrase is different from saved in the keychain. + No comment provided by engineer. + + + Database passphrase is required to open chat. + Database passphrase is required to open chat. + No comment provided by engineer. + + + Database will be encrypted and the passphrase stored in the keychain. + + Database will be encrypted and the passphrase stored in the keychain. + + No comment provided by engineer. + + + Database will be encrypted. + + Database will be encrypted. + No comment provided by engineer. @@ -743,6 +836,56 @@ Enable periodic notifications? No comment provided by engineer. + + Encrypt + Encrypt + No comment provided by engineer. + + + Encrypt database? + Encrypt database? + No comment provided by engineer. + + + Encrypted database + Encrypted database + No comment provided by engineer. + + + Encrypted message or another event + Encrypted message or another event + notification + + + Encrypted message: database error + Encrypted message: database error + notification + + + Encrypted message: keychain error + Encrypted message: keychain error + notification + + + Encrypted message: no passphrase + Encrypted message: no passphrase + notification + + + Encrypted message: unexpeсted error + Encrypted message: unexpeсted error + notification + + + Enter correct passphrase. + Enter correct passphrase. + No comment provided by engineer. + + + Enter passphrase… + Enter passphrase… + No comment provided by engineer. + Error accessing database file Error accessing database file @@ -783,6 +926,11 @@ Error enabling notifications No comment provided by engineer. + + Error encrypting database + Error encrypting database + No comment provided by engineer. + Error exporting chat database Error exporting chat database @@ -863,6 +1011,11 @@ File will be received when your contact is online, please wait or check later! No comment provided by engineer. + + File: %@ + File: %@ + No comment provided by engineer. + For console For console @@ -1053,6 +1206,13 @@ Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant push notifications will be hidden! + + Instant push notifications will be hidden! + + No comment provided by engineer. + Instantly Instantly @@ -1128,6 +1288,11 @@ We will be adding server redundancy to prevent lost messages. Joining group No comment provided by engineer. + + Keychain error + Keychain error + No comment provided by engineer. + Large file! Large file! @@ -1278,6 +1443,11 @@ We will be adding server redundancy to prevent lost messages. New message notification + + New passphrase… + New passphrase… + No comment provided by engineer. + No No @@ -1363,6 +1533,16 @@ We will be adding server redundancy to prevent lost messages. Open Settings No comment provided by engineer. + + Open chat + Open chat + No comment provided by engineer. + + + Open chat console + Open chat console + authentication reason + Open-source protocol and code – anybody can run the servers. Open-source protocol and code – anybody can run the servers. @@ -1423,11 +1603,26 @@ We will be adding server redundancy to prevent lost messages. Please check your network connection and try again. No comment provided by engineer. + + Please enter correct current passphrase. + Please enter correct current passphrase. + No comment provided by engineer. + Please restart the app and migrate the database to enable push notifications. Please restart the app and migrate the database to enable push notifications. No comment provided by engineer. + + Please store passphrase securely, you will NOT be able to access chat if you lose it. + Please store passphrase securely, you will NOT be able to access chat if you lose it. + No comment provided by engineer. + + + Please store passphrase securely, you will NOT be able to change it if you lose it. + Please store passphrase securely, you will NOT be able to change it if you lose it. + No comment provided by engineer. + Privacy & security Privacy & security @@ -1518,6 +1713,11 @@ We will be adding server redundancy to prevent lost messages. Remove member? No comment provided by engineer. + + Remove passphrase from keychain? + Remove passphrase from keychain? + No comment provided by engineer. + Reply Reply @@ -1588,6 +1788,16 @@ We will be adding server redundancy to prevent lost messages. Save group profile No comment provided by engineer. + + Save passphrase and open chat + Save passphrase and open chat + No comment provided by engineer. + + + Save passphrase in Keychain + Save passphrase in Keychain + No comment provided by engineer. + Saved SMP servers will be removed Saved SMP servers will be removed @@ -1648,6 +1858,11 @@ We will be adding server redundancy to prevent lost messages. Set contact name… No comment provided by engineer. + + Set passphrase to export + Set passphrase to export + No comment provided by engineer. + Set timeouts for proxy/VPN Set timeouts for proxy/VPN @@ -1723,6 +1938,11 @@ We will be adding server redundancy to prevent lost messages. Stop No comment provided by engineer. + + Stop SimpleX + Stop SimpleX + authentication reason + Stop chat to enable database actions Stop chat to enable database actions @@ -1940,6 +2160,16 @@ You will be prompted to complete authentication before this feature is enabled.< Unexpected migration state No comment provided by engineer. + + Unknown database error: %@ + Unknown database error: %@ + No comment provided by engineer. + + + Unknown error + Unknown error + No comment provided by engineer. + Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection. @@ -1957,11 +2187,21 @@ To connect, please ask your contact to create another connection link and check Unmute No comment provided by engineer. + + Update + Update + No comment provided by engineer. + Update .onion hosts setting? Update .onion hosts setting? No comment provided by engineer. + + Update database passphrase + Update database passphrase + No comment provided by engineer. + Update network settings? Update network settings? @@ -2032,6 +2272,16 @@ To connect, please ask your contact to create another connection link and check When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. No comment provided by engineer. + + Wrong database passphrase + Wrong database passphrase + No comment provided by engineer. + + + Wrong passphrase! + Wrong passphrase! + No comment provided by engineer. + You You @@ -2097,6 +2347,11 @@ To connect, please ask your contact to create another connection link and check You could not be verified; please try again. No comment provided by engineer. + + You have to enter passphrase every time the app starts - it is not stored on the device. + You have to enter passphrase every time the app starts - it is not stored on the device. + No comment provided by engineer. + You invited your contact You invited your contact @@ -2182,6 +2437,11 @@ To connect, please ask your contact to create another connection link and check Your chat database No comment provided by engineer. + + Your chat database is not encrypted - set passphrase to encrypt it. + Your chat database is not encrypted - set passphrase to encrypt it. + No comment provided by engineer. + Your chat profile Your chat profile @@ -2366,7 +2626,7 @@ SimpleX servers cannot see your profile. connecting (introduction invitation) No comment provided by engineer. - + connecting call… connecting call… call status @@ -2451,6 +2711,16 @@ SimpleX servers cannot see your profile. group profile updated snd group event chat item + + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. + No comment provided by engineer. + + + iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications. + iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications. + No comment provided by engineer. + incognito via contact address link incognito via contact address link diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1 @@ + diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/Localizable.strings index dc52cfe6fa..cf485752ea 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/Localizable.strings @@ -13,6 +13,9 @@ /* No comment provided by engineer. */ "~strike~" = "\\~strike~"; +/* call status */ +"connecting call" = "connecting call…"; + /* No comment provided by engineer. */ "Connecting server…" = "Connecting to server…"; diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index d0ddbd1bed..e3c08525c8 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -127,6 +127,11 @@ **Вставить полученную ссылку**, или откройте её в браузере и нажмите **Open in mobile app**. No comment provided by engineer. + + **Please note**: you will NOT be able to recover or change passphrase if you lose it. + **Внимание**: вы не сможете восстановить или поменять пароль, если вы его потеряете. + No comment provided by engineer. + **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. **Рекомендовано**: токен устройства и уведомления отправляются на сервер SimpleX Chat, но сервер не получает сами сообщения, их размер или от кого они. @@ -137,6 +142,11 @@ **Сканировать QR код**: соединиться с вашим контактом при встрече или во время видеозвонка. No comment provided by engineer. + + **Warning**: Instant push notifications require passphrase saved in Keychain. + **Внимание**: для работы мгновенных уведомлений пароль должен быть сохранен в Keychain. + No comment provided by engineer. + **e2e encrypted** audio call **e2e зашифрованный** аудиозвонок @@ -303,6 +313,16 @@ Отменить No comment provided by engineer. + + Cannot access keychain to save database password + Ошибка доступа к Keychain при сохранении пароля + No comment provided by engineer. + + + Change database passphrase? + Поменять пароль базы данных? + No comment provided by engineer. + Chat archive Архив чата @@ -346,7 +366,7 @@ Chats Чаты - back button to return to chats list + No comment provided by engineer. Choose file @@ -388,6 +408,11 @@ Подтвердить No comment provided by engineer. + + Confirm new passphrase… + Подтвердите новый пароль… + No comment provided by engineer. + Connect Соединиться @@ -528,6 +553,11 @@ Дата создания %@ No comment provided by engineer. + + Current passphrase… + Текущий пароль… + No comment provided by engineer. + Currently maximum supported file size is %@. Максимальный размер файла - %@. @@ -543,9 +573,72 @@ ID базы данных No comment provided by engineer. - - Database export & import - Экспорт и импорт архива чата + + Database encrypted! + База данных зашифрована! + No comment provided by engineer. + + + Database encryption passphrase will be updated and stored in the keychain. + + Пароль базы данных будет изменен и сохранен в Keychain. + + No comment provided by engineer. + + + Database encryption passphrase will be updated. + + Пароль базы данных будет изменен. + + No comment provided by engineer. + + + Database error + Ошибка базы данных + No comment provided by engineer. + + + Database is encrypted using a random passphrase, you can change it. + База данных зашифрована случайным паролем, вы можете его поменять. + No comment provided by engineer. + + + Database is encrypted using a random passphrase. Please change it before exporting. + База данных зашифрована случайным паролем. Пожалуйста, поменяйте его перед экспортом. + No comment provided by engineer. + + + Database passphrase + Пароль базы данных + No comment provided by engineer. + + + Database passphrase & export + Пароль и экспорт базы + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + Пароль базы данных отличается от сохраненного в Keychain. + No comment provided by engineer. + + + Database passphrase is required to open chat. + Введите пароль базы данных чтобы открыть чат. + No comment provided by engineer. + + + Database will be encrypted and the passphrase stored in the keychain. + + База данных будет зашифрована и пароль сохранен в Keychain. + + No comment provided by engineer. + + + Database will be encrypted. + + База данных будет зашифрована. + No comment provided by engineer. @@ -743,6 +836,56 @@ Включить периодические уведомления? No comment provided by engineer. + + Encrypt + Зашифровать + No comment provided by engineer. + + + Encrypt database? + Зашифровать базу данных? + No comment provided by engineer. + + + Encrypted database + База данных зашифрована + No comment provided by engineer. + + + Encrypted message or another event + Зашифрованное сообщение или событие чата + notification + + + Encrypted message: database error + Зашифрованное сообщение: ошибка базы данных + notification + + + Encrypted message: keychain error + Зашифрованное сообщение: ошибка Keychain + notification + + + Encrypted message: no passphrase + Зашифрованное сообщение: пароль не сохранен + notification + + + Encrypted message: unexpeсted error + Зашифрованное сообщение: неожиданная ошибка + notification + + + Enter correct passphrase. + Введите правильный пароль. + No comment provided by engineer. + + + Enter passphrase… + Введите пароль… + No comment provided by engineer. + Error accessing database file Ошибка при доступе к данным чата @@ -783,6 +926,11 @@ Ошибка при включении уведомлений No comment provided by engineer. + + Error encrypting database + Ошибка при шифровании + No comment provided by engineer. + Error exporting chat database Ошибка при экспорте архива чата @@ -863,6 +1011,11 @@ Файл будет принят, когда ваш контакт будет в сети, подождите или проверьте позже! No comment provided by engineer. + + File: %@ + Файл: %@ + No comment provided by engineer. + For console Для консоли @@ -1053,6 +1206,13 @@ [SimpleX Chat для терминала](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant push notifications will be hidden! + + Мгновенные уведомления будут спрятаны! + + No comment provided by engineer. + Instantly Мгновенно @@ -1128,6 +1288,11 @@ We will be adding server redundancy to prevent lost messages. Вступление в группу No comment provided by engineer. + + Keychain error + Ошибка Keychain + No comment provided by engineer. + Large file! Большой файл! @@ -1278,6 +1443,11 @@ We will be adding server redundancy to prevent lost messages. Новое сообщение notification + + New passphrase… + Новый пароль… + No comment provided by engineer. + No Нет @@ -1363,6 +1533,16 @@ We will be adding server redundancy to prevent lost messages. Открыть Настройки No comment provided by engineer. + + Open chat + Открыть чат + No comment provided by engineer. + + + Open chat console + Открыть консоль + authentication reason + Open-source protocol and code – anybody can run the servers. Открытый протокол и код - кто угодно может запустить сервер. @@ -1423,11 +1603,26 @@ We will be adding server redundancy to prevent lost messages. Пожалуйста, проверьте ваше соединение с сетью и попробуйте еще раз. No comment provided by engineer. + + Please enter correct current passphrase. + Пожалуйста, введите правильный пароль. + No comment provided by engineer. + Please restart the app and migrate the database to enable push notifications. Пожалуйста, перезапустите приложение и переместите данные чата, чтобы включить доставку уведомлений. No comment provided by engineer. + + Please store passphrase securely, you will NOT be able to access chat if you lose it. + Пожалуйста, надежно сохраните пароль, вы НЕ сможете открыть чат, если вы потеряете пароль. + No comment provided by engineer. + + + Please store passphrase securely, you will NOT be able to change it if you lose it. + Пожалуйста, надежно сохраните пароль, вы НЕ сможете его поменять, если потеряете. + No comment provided by engineer. + Privacy & security Конфиденциальность @@ -1518,6 +1713,11 @@ We will be adding server redundancy to prevent lost messages. Удалить члена группы? No comment provided by engineer. + + Remove passphrase from keychain? + Удалить пароль из Keychain? + No comment provided by engineer. + Reply Ответить @@ -1588,6 +1788,16 @@ We will be adding server redundancy to prevent lost messages. Сохранить профиль группы No comment provided by engineer. + + Save passphrase and open chat + Сохранить пароль и открыть чат + No comment provided by engineer. + + + Save passphrase in Keychain + Сохранить пароль в Keychain + No comment provided by engineer. + Saved SMP servers will be removed Сохраненные SMP серверы будут удалены @@ -1648,6 +1858,11 @@ We will be adding server redundancy to prevent lost messages. Имя контакта… No comment provided by engineer. + + Set passphrase to export + Установите пароль + No comment provided by engineer. + Set timeouts for proxy/VPN Установить таймауты для прокси/VPN @@ -1723,6 +1938,11 @@ We will be adding server redundancy to prevent lost messages. Остановить No comment provided by engineer. + + Stop SimpleX + Остановить SimpleX + authentication reason + Stop chat to enable database actions Остановите чат, чтобы разблокировать операции с архивом чата @@ -1940,6 +2160,16 @@ You will be prompted to complete authentication before this feature is enabled.< Неожиданная ошибка при перемещении данных чата No comment provided by engineer. + + Unknown database error: %@ + Неизвестная ошибка базы данных: %@ + No comment provided by engineer. + + + Unknown error + Неизвестная ошибка + No comment provided by engineer. + Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection. @@ -1957,11 +2187,21 @@ To connect, please ask your contact to create another connection link and check Уведомлять No comment provided by engineer. + + Update + Обновить + No comment provided by engineer. + Update .onion hosts setting? Обновить настройки .onion хостов? No comment provided by engineer. + + Update database passphrase + Поменять пароль + No comment provided by engineer. + Update network settings? Обновить настройки сети? @@ -2032,6 +2272,16 @@ To connect, please ask your contact to create another connection link and check Когда вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом. No comment provided by engineer. + + Wrong database passphrase + Неправильный пароль базы данных + No comment provided by engineer. + + + Wrong passphrase! + Неправильный пароль! + No comment provided by engineer. + You Вы @@ -2097,6 +2347,11 @@ To connect, please ask your contact to create another connection link and check Верификация не удалась; пожалуйста, попробуйте ещё раз. No comment provided by engineer. + + You have to enter passphrase every time the app starts - it is not stored on the device. + Пароль не сохранен на устройстве — вы будете должны ввести его при каждом запуске чата. + No comment provided by engineer. + You invited your contact Вы пригласили ваш контакт @@ -2179,7 +2434,12 @@ To connect, please ask your contact to create another connection link and check Your chat database - Данные чата + База данных + No comment provided by engineer. + + + Your chat database is not encrypted - set passphrase to encrypt it. + База данных НЕ зашифрована. Установите пароль, чтобы защитить ваши данные. No comment provided by engineer. @@ -2366,7 +2626,7 @@ SimpleX серверы не могут получить доступ к ваше соединяется (приглашение по представлению) No comment provided by engineer. - + connecting call… звонок соединяется… call status @@ -2451,6 +2711,16 @@ SimpleX серверы не могут получить доступ к ваше профиль группы обновлен snd group event chat item + + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. + iOS Keychain используется для безопасного хранения пароля - это позволяет получать мгновенные уведомления. + No comment provided by engineer. + + + iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications. + Пароль базы данных будет безопасно сохранен в iOS Keychain после запуска чата или изменения пароля - это позволит получать мгновенные уведомления. + No comment provided by engineer. + incognito via contact address link инкогнито через ссылку-контакт diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1 @@ + diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/Localizable.strings index dc52cfe6fa..cf485752ea 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/Localizable.strings @@ -13,6 +13,9 @@ /* No comment provided by engineer. */ "~strike~" = "\\~strike~"; +/* call status */ +"connecting call" = "connecting call…"; + /* No comment provided by engineer. */ "Connecting server…" = "Connecting to server…"; diff --git a/apps/ios/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/apps/ios/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1 @@ + diff --git a/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings index e08978ade2..ab09b0ac2b 100644 --- a/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings @@ -38,7 +38,7 @@ "calling…" = "входящий звонок…"; /* call status */ -"connecting call…" = "звонок соединяется…"; +"connecting call" = "звонок соединяется…"; /* chat list item title */ "connecting…" = "соединяется…"; diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 46cfbb2b8c..c122662af3 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -253,6 +253,7 @@ 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 = ""; }; 5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = ""; }; @@ -1026,6 +1027,7 @@ isa = PBXVariantGroup; children = ( 5CB0BA8A2826CB3A00B3292C /* ru */, + 5C9CC7B128D1F8F400BEF955 /* en */, ); name = Localizable.strings; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 9f2ff52e5f..f946f21395 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1355,7 +1355,7 @@ public enum CICallStatus: String, Decodable { case .missed: return NSLocalizedString("missed call", comment: "call status") case .rejected: return NSLocalizedString("rejected call", comment: "call status") case .accepted: return NSLocalizedString("accepted call", comment: "call status") - case .negotiated: return NSLocalizedString("connecting call…", comment: "call status") + case .negotiated: return NSLocalizedString("connecting call", comment: "call status") case .progress: return NSLocalizedString("call in progress", comment: "call status") case .ended: return String.localizedStringWithFormat(NSLocalizedString("ended call %@", comment: "call status"), CICallStatus.durationText(sec)) case .error: return NSLocalizedString("call error", comment: "call status") diff --git a/apps/ios/SimpleXChat/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift index 6432b36506..74f03a1bf3 100644 --- a/apps/ios/SimpleXChat/Notifications.swift +++ b/apps/ios/SimpleXChat/Notifications.swift @@ -129,7 +129,7 @@ public func createErrorNtf(_ dbStatus: DBMigrationResult) -> UNMutableNotificati case .errorKeychain: title = NSLocalizedString("Encrypted message: keychain error", comment: "notification") case .unknown: - title = NSLocalizedString("Encrypted message: unexpexted error", comment: "notification") + title = NSLocalizedString("Encrypted message: unexpeсted error", comment: "notification") case .ok: title = NSLocalizedString("Encrypted message or another event", comment: "notification") } diff --git a/apps/ios/en.lproj/Localizable.strings b/apps/ios/en.lproj/Localizable.strings index dc52cfe6fa..cf485752ea 100644 --- a/apps/ios/en.lproj/Localizable.strings +++ b/apps/ios/en.lproj/Localizable.strings @@ -13,6 +13,9 @@ /* No comment provided by engineer. */ "~strike~" = "\\~strike~"; +/* call status */ +"connecting call" = "connecting call…"; + /* No comment provided by engineer. */ "Connecting server…" = "Connecting to server…"; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index bed8f2d341..e0fc50632d 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -52,12 +52,18 @@ /* No comment provided by engineer. */ "**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Вставить полученную ссылку**, или откройте её в браузере и нажмите **Open in mobile app**."; +/* No comment provided by engineer. */ +"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Внимание**: вы не сможете восстановить или поменять пароль, если вы его потеряете."; + /* No comment provided by engineer. */ "**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Рекомендовано**: токен устройства и уведомления отправляются на сервер SimpleX Chat, но сервер не получает сами сообщения, их размер или от кого они."; /* No comment provided by engineer. */ "**Scan QR code**: to connect to your contact in person or via video call." = "**Сканировать QR код**: соединиться с вашим контактом при встрече или во время видеозвонка."; +/* No comment provided by engineer. */ +"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Внимание**: для работы мгновенных уведомлений пароль должен быть сохранен в Keychain."; + /* No comment provided by engineer. */ "*bold*" = "\\*жирный*"; @@ -218,6 +224,12 @@ /* No comment provided by engineer. */ "Cancel" = "Отменить"; +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Ошибка доступа к Keychain при сохранении пароля"; + +/* No comment provided by engineer. */ +"Change database passphrase?" = "Поменять пароль базы данных?"; + /* No comment provided by engineer. */ "Chat archive" = "Архив чата"; @@ -275,6 +287,9 @@ /* No comment provided by engineer. */ "Confirm" = "Подтвердить"; +/* No comment provided by engineer. */ +"Confirm new passphrase…" = "Подтвердите новый пароль…"; + /* No comment provided by engineer. */ "Connect" = "Соединиться"; @@ -315,7 +330,7 @@ "connecting (introduction invitation)" = "соединяется (приглашение по представлению)"; /* call status */ -"connecting call…" = "звонок соединяется…"; +"connecting call" = "звонок соединяется…"; /* No comment provided by engineer. */ "Connecting server…" = "Устанавливается соединение с сервером…"; @@ -401,6 +416,9 @@ /* No comment provided by engineer. */ "creator" = "создатель"; +/* No comment provided by engineer. */ +"Current passphrase…" = "Текущий пароль…"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Максимальный размер файла - %@."; @@ -408,11 +426,44 @@ "Dark" = "Тёмная"; /* No comment provided by engineer. */ -"Database export & import" = "Экспорт и импорт архива чата"; +"Database encrypted!" = "База данных зашифрована!"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated and stored in the keychain.\n" = "Пароль базы данных будет изменен и сохранен в Keychain.\n"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated.\n" = "Пароль базы данных будет изменен.\n"; + +/* No comment provided by engineer. */ +"Database error" = "Ошибка базы данных"; /* No comment provided by engineer. */ "Database ID" = "ID базы данных"; +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase, you can change it." = "База данных зашифрована случайным паролем, вы можете его поменять."; + +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase. Please change it before exporting." = "База данных зашифрована случайным паролем. Пожалуйста, поменяйте его перед экспортом."; + +/* No comment provided by engineer. */ +"Database passphrase" = "Пароль базы данных"; + +/* No comment provided by engineer. */ +"Database passphrase & export" = "Пароль и экспорт базы"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Пароль базы данных отличается от сохраненного в Keychain."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Введите пароль базы данных чтобы открыть чат."; + +/* No comment provided by engineer. */ +"Database will be encrypted and the passphrase stored in the keychain.\n" = "База данных будет зашифрована и пароль сохранен в Keychain.\n"; + +/* No comment provided by engineer. */ +"Database will be encrypted.\n" = "База данных будет зашифрована.\n"; + /* No comment provided by engineer. */ "Database will be migrated when the app restarts" = "Данные чата будут мигрированы при перезапуске"; @@ -545,12 +596,42 @@ /* No comment provided by engineer. */ "Enable TCP keep-alive" = "Включить TCP keep-alive"; +/* No comment provided by engineer. */ +"Encrypt" = "Зашифровать"; + +/* No comment provided by engineer. */ +"Encrypt database?" = "Зашифровать базу данных?"; + +/* No comment provided by engineer. */ +"Encrypted database" = "База данных зашифрована"; + +/* notification */ +"Encrypted message or another event" = "Зашифрованное сообщение или событие чата"; + +/* notification */ +"Encrypted message: database error" = "Зашифрованное сообщение: ошибка базы данных"; + +/* notification */ +"Encrypted message: keychain error" = "Зашифрованное сообщение: ошибка Keychain"; + +/* notification */ +"Encrypted message: no passphrase" = "Зашифрованное сообщение: пароль не сохранен"; + +/* notification */ +"Encrypted message: unexpeсted error" = "Зашифрованное сообщение: неожиданная ошибка"; + /* No comment provided by engineer. */ "ended" = "завершён"; /* call status */ "ended call %@" = "завершённый звонок %@"; +/* No comment provided by engineer. */ +"Enter correct passphrase." = "Введите правильный пароль."; + +/* No comment provided by engineer. */ +"Enter passphrase…" = "Введите пароль…"; + /* No comment provided by engineer. */ "error" = "ошибка"; @@ -578,6 +659,9 @@ /* No comment provided by engineer. */ "Error enabling notifications" = "Ошибка при включении уведомлений"; +/* No comment provided by engineer. */ +"Error encrypting database" = "Ошибка при шифровании"; + /* No comment provided by engineer. */ "Error exporting chat database" = "Ошибка при экспорте архива чата"; @@ -626,6 +710,9 @@ /* No comment provided by engineer. */ "File will be received when your contact is online, please wait or check later!" = "Файл будет принят, когда ваш контакт будет в сети, подождите или проверьте позже!"; +/* No comment provided by engineer. */ +"File: %@" = "Файл: %@"; + /* No comment provided by engineer. */ "For console" = "Для консоли"; @@ -755,6 +842,9 @@ /* No comment provided by engineer. */ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "[SimpleX Chat для терминала](https://github.com/simplex-chat/simplex-chat)"; +/* No comment provided by engineer. */ +"Instant push notifications will be hidden!\n" = "Мгновенные уведомления будут спрятаны!\n"; + /* No comment provided by engineer. */ "Instantly" = "Мгновенно"; @@ -782,6 +872,12 @@ /* chat list item title */ "invited to connect" = "приглашение"; +/* No comment provided by engineer. */ +"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "iOS Keychain используется для безопасного хранения пароля - это позволяет получать мгновенные уведомления."; + +/* No comment provided by engineer. */ +"iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "Пароль базы данных будет безопасно сохранен в iOS Keychain после запуска чата или изменения пароля - это позволит получать мгновенные уведомления."; + /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Это позволяет иметь много анонимных соединений без общих данных между ними в одном профиле пользователя."; @@ -812,6 +908,9 @@ /* No comment provided by engineer. */ "Joining group" = "Вступление в группу"; +/* No comment provided by engineer. */ +"Keychain error" = "Ошибка Keychain"; + /* No comment provided by engineer. */ "Large file!" = "Большой файл!"; @@ -920,6 +1019,9 @@ /* notification */ "New message" = "Новое сообщение"; +/* No comment provided by engineer. */ +"New passphrase…" = "Новый пароль…"; + /* No comment provided by engineer. */ "No" = "Нет"; @@ -971,6 +1073,12 @@ /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Только пользовательские устройства хранят контакты, группы и сообщения, которые отправляются **с двухуровневым end-to-end шифрованием**"; +/* No comment provided by engineer. */ +"Open chat" = "Открыть чат"; + +/* authentication reason */ +"Open chat console" = "Открыть консоль"; + /* No comment provided by engineer. */ "Open Settings" = "Открыть Настройки"; @@ -1019,9 +1127,18 @@ /* No comment provided by engineer. */ "Please check your network connection and try again." = "Пожалуйста, проверьте ваше соединение с сетью и попробуйте еще раз."; +/* No comment provided by engineer. */ +"Please enter correct current passphrase." = "Пожалуйста, введите правильный пароль."; + /* No comment provided by engineer. */ "Please restart the app and migrate the database to enable push notifications." = "Пожалуйста, перезапустите приложение и переместите данные чата, чтобы включить доставку уведомлений."; +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Пожалуйста, надежно сохраните пароль, вы НЕ сможете открыть чат, если вы потеряете пароль."; + +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to change it if you lose it." = "Пожалуйста, надежно сохраните пароль, вы НЕ сможете его поменять, если потеряете."; + /* No comment provided by engineer. */ "Privacy & security" = "Конфиденциальность"; @@ -1085,6 +1202,9 @@ /* No comment provided by engineer. */ "Remove member?" = "Удалить члена группы?"; +/* No comment provided by engineer. */ +"Remove passphrase from keychain?" = "Удалить пароль из Keychain?"; + /* No comment provided by engineer. */ "removed" = "удален(а)"; @@ -1130,6 +1250,12 @@ /* No comment provided by engineer. */ "Save group profile" = "Сохранить профиль группы"; +/* No comment provided by engineer. */ +"Save passphrase and open chat" = "Сохранить пароль и открыть чат"; + +/* No comment provided by engineer. */ +"Save passphrase in Keychain" = "Сохранить пароль в Keychain"; + /* No comment provided by engineer. */ "Saved SMP servers will be removed" = "Сохраненные SMP серверы будут удалены"; @@ -1172,6 +1298,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Имя контакта…"; +/* No comment provided by engineer. */ +"Set passphrase to export" = "Установите пароль"; + /* No comment provided by engineer. */ "Set timeouts for proxy/VPN" = "Установить таймауты для прокси/VPN"; @@ -1235,6 +1364,9 @@ /* No comment provided by engineer. */ "Stop chat?" = "Остановить чат?"; +/* authentication reason */ +"Stop SimpleX" = "Остановить SimpleX"; + /* No comment provided by engineer. */ "strike" = "зачеркнуть"; @@ -1364,6 +1496,12 @@ /* connection info */ "unknown" = "неизвестно"; +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Неизвестная ошибка базы данных: %@"; + +/* No comment provided by engineer. */ +"Unknown error" = "Неизвестная ошибка"; + /* No comment provided by engineer. */ "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Возможно, ваш контакт удалил ссылку, или она уже была использована. Если это не так, то это может быть ошибкой - пожалуйста, сообщите нам об этом.\nЧтобы установить соединение, попросите ваш контакт создать еще одну ссылку и проверьте ваше соединение с сетью."; @@ -1373,9 +1511,15 @@ /* No comment provided by engineer. */ "Unmute" = "Уведомлять"; +/* No comment provided by engineer. */ +"Update" = "Обновить"; + /* No comment provided by engineer. */ "Update .onion hosts setting?" = "Обновить настройки .onion хостов?"; +/* No comment provided by engineer. */ +"Update database passphrase" = "Поменять пароль"; + /* No comment provided by engineer. */ "Update network settings?" = "Обновить настройки сети?"; @@ -1445,6 +1589,12 @@ /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Когда вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом."; +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Неправильный пароль базы данных"; + +/* No comment provided by engineer. */ +"Wrong passphrase!" = "Неправильный пароль!"; + /* No comment provided by engineer. */ "You" = "Вы"; @@ -1487,6 +1637,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Верификация не удалась; пожалуйста, попробуйте ещё раз."; +/* No comment provided by engineer. */ +"You have to enter passphrase every time the app starts - it is not stored on the device." = "Пароль не сохранен на устройстве — вы будете должны ввести его при каждом запуске чата."; + /* No comment provided by engineer. */ "You invited your contact" = "Вы пригласили ваш контакт"; @@ -1545,7 +1698,10 @@ "Your chat address" = "Ваш SimpleX адрес"; /* No comment provided by engineer. */ -"Your chat database" = "Данные чата"; +"Your chat database" = "База данных"; + +/* No comment provided by engineer. */ +"Your chat database is not encrypted - set passphrase to encrypt it." = "База данных НЕ зашифрована. Установите пароль, чтобы защитить ваши данные."; /* No comment provided by engineer. */ "Your chat profile" = "Ваш профиль"; From f1e34531c25e9a1801a40fddf8860efe6193e444 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Wed, 14 Sep 2022 20:16:13 +0400 Subject: [PATCH 41/44] mobile: fix db encryption translations (#1049) --- apps/android/app/src/main/res/values-ru/strings.xml | 12 ++++++------ .../ru.xcloc/Localized Contents/ru.xliff | 4 ++-- apps/ios/ru.lproj/Localizable.strings | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index a9ae7ff7e3..b02352afc1 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -69,8 +69,8 @@ Приложение периодически получает новые сообщения — это потребляет несколько процентов батареи в день. Приложение не использует push уведомления — данные не отправляются с вашего устройства на сервер. Введите пароль Для получения уведомлений, пожалуйста, введите пароль от базы данных - Ошибка данных - Ошибка при инициализации данных. Нажмите чтобы узнать больше + Ошибка базы данных + Ошибка при инициализации базы данных. Нажмите чтобы узнать больше SimpleX Chat сервис @@ -573,7 +573,7 @@ База данных НЕ зашифрована. Установите пароль, чтобы защитить ваши данные. Android Keystore используется для безопасного хранения пароля - это позволяет стабильно получать уведомления в фоновом режиме. База данных зашифрована случайным паролем, вы можете его поменять. - Внимание: вы не сможете восстановить или поменять пароль, если вы его потеряете. + Внимание: вы не сможете восстановить или поменять пароль, если потеряете его. Пароль базы данных будет безопасно сохранен в Android Keystore после запуска чата или изменения пароля - это позволит стабильно получать уведомления. Пароль не сохранен на устройстве — вы будете должны ввести его при каждом запуске чата. Зашифровать базу данных? @@ -583,16 +583,16 @@ Пароль базы данных будет изменен и сохранен в Keystore. Пароль базы данных будет изменен. Пожалуйста, надежно сохраните пароль, вы НЕ сможете его поменять, если потеряете. - Пожалуйста, надежно сохраните пароль, вы НЕ сможете открыть чат, если вы потеряете пароль. + Пожалуйста, надежно сохраните пароль, вы НЕ сможете открыть чат, если потеряете его. Неправильный пароль базы данных База данных зашифрована Ошибка базы данных Ошибка Keystore - Пароль базы данных отличается от пароля сохрененного в Keystore. + Пароль базы данных отличается от сохраненного в Keystore. Файл: %s - Введите пароль базы данных чтобы открыть чат. + Введите пароль базы данных, чтобы открыть чат. Ошибка: %s Невозможно сохранить пароль в Keystore Неизвестная ошибка базы данных: %s diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index e3c08525c8..7a869d5739 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -1209,7 +1209,7 @@ Instant push notifications will be hidden! - Мгновенные уведомления будут спрятаны! + Мгновенные уведомления будут скрыты! No comment provided by engineer. @@ -1615,7 +1615,7 @@ We will be adding server redundancy to prevent lost messages. Please store passphrase securely, you will NOT be able to access chat if you lose it. - Пожалуйста, надежно сохраните пароль, вы НЕ сможете открыть чат, если вы потеряете пароль. + Пожалуйста, надежно сохраните пароль, вы НЕ сможете открыть чат, если потеряете его. No comment provided by engineer. diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index e0fc50632d..c90a2a31c7 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -843,7 +843,7 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "[SimpleX Chat для терминала](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Мгновенные уведомления будут спрятаны!\n"; +"Instant push notifications will be hidden!\n" = "Мгновенные уведомления будут скрыты!\n"; /* No comment provided by engineer. */ "Instantly" = "Мгновенно"; @@ -1134,7 +1134,7 @@ "Please restart the app and migrate the database to enable push notifications." = "Пожалуйста, перезапустите приложение и переместите данные чата, чтобы включить доставку уведомлений."; /* No comment provided by engineer. */ -"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Пожалуйста, надежно сохраните пароль, вы НЕ сможете открыть чат, если вы потеряете пароль."; +"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Пожалуйста, надежно сохраните пароль, вы НЕ сможете открыть чат, если потеряете его."; /* No comment provided by engineer. */ "Please store passphrase securely, you will NOT be able to change it if you lose it." = "Пожалуйста, надежно сохраните пароль, вы НЕ сможете его поменять, если потеряете."; From 29b27fa602494a6257b73a2644d2904027a9610f Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 14 Sep 2022 23:27:17 +0300 Subject: [PATCH 42/44] android: progress indicator in database related views (#1052) Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../views/database/DatabaseEncryptionView.kt | 34 ++++++++++++------- .../app/views/database/DatabaseErrorView.kt | 34 ++++++++++++++++--- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt index 40726a4e55..3298ead4ac 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt @@ -58,6 +58,7 @@ fun DatabaseEncryptionView(m: ChatModel) { confirmNewKey, storedKey, initialRandomDBPassphrase, + progressIndicator, onConfirmEncrypt = { progressIndicator.value = true withApi { @@ -127,6 +128,7 @@ fun DatabaseEncryptionLayout( confirmNewKey: MutableState, storedKey: MutableState, initialRandomDBPassphrase: MutableState, + progressIndicator: MutableState, onConfirmEncrypt: () -> Unit, ) { Column( @@ -140,7 +142,7 @@ fun DatabaseEncryptionLayout( ) SectionView(null) { - SavePassphraseSetting(useKeychain.value, initialRandomDBPassphrase.value, storedKey.value) { checked -> + SavePassphraseSetting(useKeychain.value, initialRandomDBPassphrase.value, storedKey.value, progressIndicator.value) { checked -> if (checked) { setUseKeychain(true, useKeychain, prefs) } else if (storedKey.value) { @@ -179,23 +181,27 @@ fun DatabaseEncryptionLayout( keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), ) val onClickUpdate = { - if (currentKey.value == "") { - if (useKeychain.value) - encryptDatabaseSavedAlert(onConfirmEncrypt) - else - encryptDatabaseAlert(onConfirmEncrypt) - } else { - if (useKeychain.value) - changeDatabaseKeySavedAlert(onConfirmEncrypt) - else - changeDatabaseKeyAlert(onConfirmEncrypt) + // Don't do things concurrently. Shouldn't be here concurrently, just in case + if (!progressIndicator.value) { + if (currentKey.value == "") { + if (useKeychain.value) + encryptDatabaseSavedAlert(onConfirmEncrypt) + else + encryptDatabaseAlert(onConfirmEncrypt) + } else { + if (useKeychain.value) + changeDatabaseKeySavedAlert(onConfirmEncrypt) + else + changeDatabaseKeyAlert(onConfirmEncrypt) + } } } val disabled = currentKey.value == newKey.value || newKey.value != confirmNewKey.value || newKey.value.isEmpty() || !validKey(currentKey.value) || - !validKey(newKey.value) + !validKey(newKey.value) || + progressIndicator.value DatabaseKeyField( confirmNewKey, @@ -280,6 +286,7 @@ fun SavePassphraseSetting( useKeychain: Boolean, initialRandomDBPassphrase: Boolean, storedKey: Boolean, + progressIndicator: Boolean, onCheckedChange: (Boolean) -> Unit, ) { SectionItemView() { @@ -303,7 +310,7 @@ fun SavePassphraseSetting( checkedThumbColor = MaterialTheme.colors.primary, uncheckedThumbColor = HighOrLowlight ), - enabled = !initialRandomDBPassphrase + enabled = !initialRandomDBPassphrase && !progressIndicator ) } } @@ -495,6 +502,7 @@ fun PreviewDatabaseEncryptionLayout() { confirmNewKey = remember { mutableStateOf("") }, storedKey = remember { mutableStateOf(true) }, initialRandomDBPassphrase = remember { mutableStateOf(true) }, + progressIndicator = remember { mutableStateOf(false) }, onConfirmEncrypt = {}, ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt index 13becac525..82454bf901 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt @@ -26,6 +26,7 @@ fun DatabaseErrorView( chatDbStatus: State, appPreferences: AppPreferences, ) { + val progressIndicator = remember { mutableStateOf(false) } val dbKey = remember { mutableStateOf("") } var storedDBKey by remember { mutableStateOf(DatabaseUtils.getDatabaseKey()) } var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) } @@ -35,7 +36,7 @@ fun DatabaseErrorView( appPreferences.storeDBPassphrase.set(true) useKeychain = true appPreferences.initialRandomDBPassphrase.set(false) - runChat(dbKey.value, chatDbStatus, appPreferences) + runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences) } val title = when (chatDbStatus.value) { is DBMigrationResult.OK -> "" @@ -63,7 +64,7 @@ fun DatabaseErrorView( Column( Modifier.padding(horizontal = 8.dp, vertical = 8.dp) ) { - val buttonEnabled = validKey(dbKey.value) + val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value when (val status = chatDbStatus.value) { is DBMigrationResult.ErrorNotADatabase -> { if (useKeychain && !storedDBKey.isNullOrEmpty()) { @@ -77,12 +78,12 @@ fun DatabaseErrorView( } else { Text(generalGetString(R.string.database_passphrase_is_required)) DatabaseKeyField(dbKey, buttonEnabled) { - if (useKeychain) saveAndRunChatOnClick() else runChat(dbKey.value, chatDbStatus, appPreferences) + if (useKeychain) saveAndRunChatOnClick() else runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences) } if (useKeychain) { SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick) } else { - OpenChatButton(buttonEnabled) { runChat(dbKey.value, chatDbStatus, appPreferences) } + OpenChatButton(buttonEnabled) { runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences) } } } } @@ -104,14 +105,37 @@ fun DatabaseErrorView( } } } + if (progressIndicator.value) { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + Modifier + .padding(horizontal = 2.dp) + .size(30.dp), + color = HighOrLowlight, + strokeWidth = 2.5.dp + ) + } + } } -private fun runChat(dbKey: String, chatDbStatus: State, prefs: AppPreferences) { +private fun runChat( + dbKey: String, + chatDbStatus: State, + progressIndicator: MutableState, + prefs: AppPreferences +) = CoroutineScope(Dispatchers.Default).launch { + // Don't do things concurrently. Shouldn't be here concurrently, just in case + if (progressIndicator.value) return@launch + progressIndicator.value = true try { SimplexApp.context.initChatController(dbKey) } catch (e: Exception) { Log.d(TAG, "initializeChat ${e.stackTraceToString()}") } + progressIndicator.value = false when (val status = chatDbStatus.value) { is DBMigrationResult.OK -> { SimplexService.cancelPassphraseNotification() From ff35a3fee5be3150a575603207ff68f2fe9d1f66 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Thu, 15 Sep 2022 00:28:21 +0400 Subject: [PATCH 43/44] core: sqlcipher stack build (#991) * wip * uncomment * comment * ci split build step * check openssl * openssl version * add stack params * update mac openssl parameters * ls openssl * clean stack.yaml * clean up build.yml Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .github/workflows/build.yml | 23 ++++++++++++++++++++++- stack.yaml | 3 ++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f5984b70b0..5b386017d5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -82,7 +82,7 @@ jobs: - name: Unix build id: unix_build - if: matrix.os != 'windows-latest' + if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-18.04' shell: bash run: | stack build --test @@ -99,6 +99,27 @@ jobs: # Unix / + # / Mac + + - name: Mac build + id: mac_build + if: matrix.os == 'macos-latest' + shell: bash + run: | + stack build --test --extra-include-dirs=/usr/local/opt/openssl@1.1/include --extra-lib-dirs=/usr/local/opt/openssl@1.1/lib + echo "::set-output name=local_install_root::$(stack path --local-install-root)" + + - name: Unix upload binary to release + if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest' + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ${{ steps.unix_build.outputs.local_install_root }}/bin/simplex-chat + asset_name: ${{ matrix.asset_name }} + tag: ${{ github.ref }} + + # Mac / + # / Windows # * In powershell multiline commands do not fail if individual commands fail - https://github.community/t/multiline-commands-on-windows-do-not-fail-if-individual-commands-fail/16753 diff --git a/stack.yaml b/stack.yaml index ad0e40179a..3f01145672 100644 --- a/stack.yaml +++ b/stack.yaml @@ -69,6 +69,8 @@ flags: zip: disable-bzip2: true disable-zstd: true + direct-sqlcipher: + openssl: true # Extra package databases containing global packages # extra-package-dbs: [] @@ -84,7 +86,6 @@ flags: # arch: x86_64 # # Extra directories used by stack for building -# extra-include-dirs: [/path/to/dir] # extra-lib-dirs: [/path/to/dir] # # Allow a newer minor version of GHC than the snapshot specifies From d1571798f4524ba406841134724e62a1aa022117 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 14 Sep 2022 21:50:44 +0100 Subject: [PATCH 44/44] update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index e2c6d2f276..ff67741368 100644 --- a/cabal.project +++ b/cabal.project @@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: afecefc3adee044545144a269ad7df7b738af65a + tag: f08d81252deb68eb4a1ce4502712b99264b6749d source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 5ab74a9df9..ca7209ffba 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."afecefc3adee044545144a269ad7df7b738af65a" = "0v3z9sfy7vjbgm3cznb7vaycn2r2yav83x74khf6a001cs1zv171"; + "https://github.com/simplex-chat/simplexmq.git"."f08d81252deb68eb4a1ce4502712b99264b6749d" = "0v3z9sfy7vjbgm3cznb7vaycn2r2yav83x74khf6a001cs1zv171"; "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; "https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; diff --git a/stack.yaml b/stack.yaml index 3f01145672..3d07ce843c 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: afecefc3adee044545144a269ad7df7b738af65a + commit: f08d81252deb68eb4a1ce4502712b99264b6749d # - ../direct-sqlcipher - github: simplex-chat/direct-sqlcipher commit: 34309410eb2069b029b8fc1872deb1e0db123294