core: change database encryption API to require current passphrase on all changes (#1019)

This commit is contained in:
Evgeny Poberezkin authored and GitHub committed 2022-09-05 14:54:39 +01:00
1 parent 229f385f42
commit 082e12683b
8 files changed
+88 -71

No files matched your search

+44 -50
View File
@@ -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)]