festung-0.9.1.1: src/Festung/Vault/Persistence.hs
{-# LANGUAGE NamedFieldPuns #-}
module Festung.Vault.Persistence
( openVault
, openVault'
, closeVault
, Error(..)
, VaultHandle
, QueryResult(..)
, executeQuery
, executeParameterizedQuery
, Value(..)
, Header
, ColumnName
, ColumnType
, Password
, VaultParameters(..)
) where
import qualified Codec.Binary.UTF8.String as UTF8
import Control.Exception.Base (assert)
import Control.Monad
import Control.Monad.Catch (finally)
import Control.Monad.Trans.Either (runEitherT, left, right, EitherT(..))
import Control.Monad.Error.Class (throwError, catchError, MonadError)
import Control.Monad.Trans (liftIO, lift)
import Data.Word
import Data.Either as E
import Foreign.C
import Foreign.Ptr
import Foreign.ForeignPtr
import Foreign.Marshal.Alloc
import Foreign.Marshal.Array
import qualified Foreign.Concurrent as Conc
import Foreign.Storable
import Data.Functor ((<$>), void)
import Data.Int (Int64)
import Database.SQLCipher.Base
import Database.SQLCipher.Types
import Text.Printf (printf)
import Festung.Utils (hoistMEither, whenJust)
{- This module basically interracts with raw C calls to SQLCipher. This
- belongs in haskell-sqlcipher, however I (Antoine) wasn't sure what should go
- there, and how the Haskell API should be structured.
-
- For this reason, I decided to put these C wrapping functions here, and see
- where it goes. The plan is to upstream this in a near future™.
-
- A lot of these functions have been inspired by haskell-sqlcipher (which is based
- on Database.SQLite)
-}
newtype VaultHandle = VaultHandle (ForeignPtr ())
instance Show VaultHandle where
show _ = "VaultHandle"
data Error = CouldNotOpenVault Status !String
| MultipleStatements
| WrongParametrization { got :: Int, expected :: Int }
| NotSupportedError String
| InternalError String
| IntegrityError Status !String
| OperationalError Status !String
| DatabaseError Status !String
| DataError Status !String
| ProgrammingError Status !String
deriving (Show)
data Value = StringValue String
| IntValue Int64
| FloatValue Double
| BlobValue [Word8]
| NullValue
deriving (Show, Eq)
type Row = [Value]
-- FIXME(Antoine): Later on this should be an Enum. (Nothing should be "DYNAMIC")
type ColumnType = Maybe String
type ColumnName = String
type Header = (ColumnName, ColumnType)
data QueryResult = QueryResult
{ rows :: [Row]
, lastRowId :: Int
, rowsChanged :: Maybe Int
, headers :: [Header]
} deriving (Show)
-- | Binary password to a vault (this password will be escaped)
type Password = [Word8]
newtype VaultParameters = VaultParameters { kdfIter :: Maybe Integer }
deriving (Eq, Show)
head' :: [a] -> Maybe a
head' [] = Nothing
head' (h:_) = Just h
valueString :: Value -> String
valueString (StringValue s) = s
valueString (IntValue i) = show i
valueString (FloatValue f) = show f
valueString (BlobValue _) = undefined
valueString NullValue = "NULL"
-- |@'encodeCString' encode a Haskell string for it to be used in C.
--
-- C functions only manipulates bytes because they use @char *@, this encodes
-- the string in UTF8, and returns a list of byte.
--
-- This utility function is only for internal use in Festung.Vault.Persistence.
encodeCString :: String -> [CChar]
encodeCString = map fromIntegral <$> UTF8.encode
-- |@'decodeCString' reverses @'encodeCString'
--
-- @decodeCString . encodeCString == id@
--
-- This utility function is only for internal use in Festung.Vault.Persistence.
decodeCString :: [CChar] -> String
decodeCString = UTF8.decode <$> map fromIntegral
withUTF8CStringLen :: String -> (CStringLen -> IO a) -> IO a
withUTF8CStringLen str action =
let cStr = encodeCString str
in withArray0 0 cStr $ \ptr -> action (ptr, length cStr)
-- |Convert a Haskell String to a @char *@ string, the time to execute an IO action.
withUTF8CString :: String -> (CString -> IO a) -> IO a
withUTF8CString str =
let cStr = encodeCString str
in withArray0 0 cStr
-- |peek @char *@ string into a Haskell String.
peekUTF8CString :: CString -> IO String
peekUTF8CString cStr = decodeCString <$> peekArray0 0 cStr
-- |Get the error string from a errorneous status code
sqlCipherErrorString :: Status -> IO String
sqlCipherErrorString = peekCString . sqlite3_errstr
sqlCipherErrorMessage :: SQLite -> IO String
sqlCipherErrorMessage db = peekUTF8CString =<< sqlite3_errmsg db
-- |Get the @'Error' object for a status code
sqlCipherError :: Status -> (Status -> String -> Error) -> IO Error
sqlCipherError status constructor = constructor status <$> sqlCipherErrorString status
sqlCipherErrorFromDb :: Status -> SQLite -> (Status -> String -> Error) -> IO Error
sqlCipherErrorFromDb status db constructor = constructor status <$> sqlCipherErrorMessage db
toError :: Status -> SQLite -> IO Error
toError status db =
let constructor = case status of
_ | status `elem`
[ sQLITE_ERROR
, sQLITE_PERM
, sQLITE_ABORT
, sQLITE_BUSY
, sQLITE_LOCKED
, sQLITE_READONLY
, sQLITE_INTERRUPT
, sQLITE_IOERR
, sQLITE_FULL
, sQLITE_CANTOPEN
, sQLITE_PROTOCOL
, sQLITE_EMPTY
, sQLITE_SCHEMA
] -> OperationalError
| status `elem`
[ sQLITE_CONSTRAINT
, sQLITE_MISMATCH
] -> IntegrityError
| status == sQLITE_CORRUPT -> DatabaseError
| status == sQLITE_TOOBIG -> DataError
| status == sQLITE_MISUSE -> ProgrammingError
| otherwise -> DatabaseError
in sqlCipherErrorFromDb status db constructor
-- |Pass @[Word8]@ to a c function with the signature @int func(void *data, int dataLen)@
withData :: [Word8] -> (Int -> Ptr () -> IO a) -> IO a
withData data_ action =
let cData = map fromIntegral data_ :: [CChar]
in withArrayLen cData $ \len dataPtr -> action len (castPtr dataPtr)
-- |Read @void *data@
peekData :: Ptr () -> Int -> IO [Word8]
peekData ptr len =
let ptr' = castPtr ptr :: Ptr CChar
in map fromIntegral <$> peekArray len ptr'
withPrim :: VaultHandle -> (SQLite -> IO a) -> IO a
withPrim (VaultHandle ptr) action = withForeignPtr ptr (action . SQLite)
liftMEither :: Monad m => m (Either e a) -> EitherT e m a
liftMEither m = lift m >>= E.either left right
-- |A computation to run, in case an error occurs.
--
-- This functions doesn't restore the state, it keeps the errored state.
onError :: (MonadError e m) => m () -> m a -> m a
onError errorAction action = catchError action $ \err -> errorAction >> throwError err
-- |Just runs @sqlite3_open@.
--
-- This functions is for internal use only.
openSQLiteDB :: FilePath -> EitherT Error IO VaultHandle
openSQLiteDB filename =
let newVaultHandle h@(SQLite ptr) = VaultHandle <$> Conc.newForeignPtr ptr sqlite3_close'
where sqlite3_close' = void $ sqlite3_close h
in liftMEither . alloca $ \dbPtr -> do
status <- withUTF8CString filename $ flip sqlite3_open dbPtr
if status == sQLITE_OK
then Right <$> (newVaultHandle =<< peek dbPtr)
else Left <$> sqlCipherError status OperationalError
-- |@'usePassword' uses SQLCipher's @sqlite3_key@ special function to send the database password.
--
-- This utility function is only for internal use in Festung.Vault.Persistence,
-- more specificaly it should only be used in @'openVault'.
usePassword :: VaultHandle -> Password -> EitherT Error IO ()
usePassword handle password = liftMEither $ withPrim handle $ \db -> do
status <- withData password $ \len passwordData ->
let cLen = fromIntegral len
in sqlite3_key db passwordData cLen
if status == sQLITE_OK
then return $ Right ()
else Left <$> toError status db
-- |@'setKdfIter' run the @PRAGMA kdf_iter@
--
-- This utility function is for internal use only
setKdfIter :: VaultHandle -> Integer -> EitherT Error IO ()
setKdfIter handle kdfIter =
let query = printf "PRAGMA kdf_iter = '%d'" kdfIter
in void $ hoistMEither $ executeQuery handle query
ensureIntegrity :: VaultHandle -> EitherT Error IO ()
ensureIntegrity handle = do
result <- liftMEither $ executeQuery handle "PRAGMA quick_check(1)"
let integrity_check = head . head . rows $ isIntegrityCheck result
unless (isOk integrity_check) $ left $ IntegrityError sQLITE_MISMATCH (valueString integrity_check)
where isOk (StringValue v) | v == "ok" = True
| otherwise = False
isIntegrityCheck r@QueryResult { headers } =
assert (fmap fst (head' headers) == Just "integrity_check") r
-- |@'openVault' opens the vault and returns an handle
openVault :: FilePath -> Password -> IO (Either Error VaultHandle)
openVault filename password = openVault' filename password noParams
where noParams = VaultParameters { kdfIter = Nothing }
openVault' :: FilePath -> Password -> VaultParameters -> IO (Either Error VaultHandle)
openVault' filename password VaultParameters{ kdfIter } = runEitherT $ do
handle <- openSQLiteDB filename
onError (liftIO $ closeVault handle) $ do
usePassword handle password
whenJust kdfIter $ setKdfIter handle
ensureIntegrity handle
return handle
-- |@'withPreparedStatement' allocate a prepared statement
--
-- This function will finalize and dealocate the statement.
--
-- This helper function is inteded
withPreparedStatement :: SQLite -> String -> (SQLiteStmt -> IO a) -> IO (Either Error a)
withPreparedStatement db query action =
alloca $ \ppStmt ->
alloca $ \pzTail ->
withArrayLen cData $ \nByte zSql -> do
let nByte' = fromIntegral nByte
status <- sqlite3_prepare db zSql nByte' ppStmt pzTail
pStmt <- peek ppStmt
runEitherT $ do
-- FIXME(Antoine): Too many liftIO
when (status /= sQLITE_OK) $
left =<< liftIO (toError status db)
when (isNullStmt pStmt) $
left $ InternalError "Prepared statement was null."
-- FIXME(Antoine): The double use of finalize is not great.
let finalize = sqlite3_finalize pStmt
-- TODO: Multiple statements
-- when (pzTail /= nullPtr) (liftIO finalize >> left MultipleStatements)
liftIO $ finally (action pStmt) finalize
where cData = encodeCString query
handleBindStatus :: Status -> SQLite -> EitherT Error IO ()
handleBindStatus status db =
if status == sQLITE_OK
then right ()
else left =<< liftIO (toError status db)
-- |@'bindParameter' binds one paramater to a statmeent.
--
-- This function is indented for internal use only.
bindParameter :: SQLite -> SQLiteStmt -> CInt -> Value -> EitherT Error IO ()
bindParameter db stmt idx (StringValue str) = do
status <- liftIO $ withUTF8CStringLen str $ \(cStr, cStrLen) ->
let cStrLen' = fromIntegral cStrLen
in sqlite3_bind_text64 stmt idx cStr cStrLen' sqlite3_transient_destructor sQLITE_UTF8
handleBindStatus status db
bindParameter db stmt idx (IntValue int) = do
status <- liftIO $ sqlite3_bind_int64 stmt idx (fromIntegral int)
handleBindStatus status db
bindParameter db stmt idx (FloatValue float) = do
status <- liftIO $ sqlite3_bind_double stmt idx float
handleBindStatus status db
bindParameter _db _stmt _idx (BlobValue _data) = left $ NotSupportedError "Binding blob is not supported yet."
bindParameter db stmt idx NullValue = do
status <- liftIO $ sqlite3_bind_null stmt idx
handleBindStatus status db
fetchColumn :: SQLiteStmt -> Int -> EitherT Error IO Value
fetchColumn stmt pos = do
let pos' = fromIntegral pos -- XXX(Antoine): This is copy pasted everywhere
ct <- liftIO $ sqlite3_column_type stmt pos'
case ct of
_ | ct == sQLITE_INTEGER ->
liftIO $ IntValue . fromIntegral <$> sqlite3_column_int64 stmt pos'
| ct == sQLITE_FLOAT ->
liftIO $ FloatValue <$> sqlite3_column_double stmt pos'
| ct == sQLITE_NULL -> return NullValue
| ct == sQLITE_TEXT -> liftIO $ do
-- TODO: Use sqlite3_column_bytes AFTER
-- TODO: Handle null pointer sqilte3_column_text
cStr <- sqlite3_column_text stmt pos'
StringValue <$> peekUTF8CString cStr
| ct == sQLITE_BLOB -> liftIO $ do
-- TODO: Handle null pointer
blob <- sqlite3_column_blob stmt pos'
blobLen <- sqlite3_column_bytes stmt pos'
BlobValue <$> peekData blob (fromIntegral blobLen)
| otherwise -> left $ ProgrammingError sQLITE_MISUSE $ "Unknown column type: " ++ show ct
fetchColumnName :: SQLiteStmt -> Int -> EitherT Error IO ColumnName
fetchColumnName stmt pos = do
let pos' = fromIntegral pos -- XXX(Antoine): This is copy pasted everywhere
columnName <- liftIO $ sqlite3_column_name stmt pos'
when (columnName == nullPtr) $ left $ InternalError "Couldn't fetch column name."
liftIO $ peekUTF8CString columnName
fetchColumnType :: SQLiteStmt -> Int -> EitherT Error IO ColumnType
fetchColumnType stmt pos = do
let pos' = fromIntegral pos -- XXX(Antoine): This is copy pasted everywhere
columnName <- liftIO $ sqlite3_column_decltype stmt pos'
if columnName == nullPtr
then return Nothing
else liftIO $ Just <$> peekUTF8CString columnName
fetchColumnHeader :: SQLiteStmt -> Int -> EitherT Error IO Header
fetchColumnHeader stmt pos =
pure (,) <*>
fetchColumnName stmt pos <*>
fetchColumnType stmt pos
mapColumns :: (SQLiteStmt -> Int -> EitherT Error IO a) -> SQLiteStmt -> EitherT Error IO [a]
mapColumns f stmt = do
nCol <- liftIO $ fromIntegral <$> sqlite3_column_count stmt
mapM (f stmt) $ take nCol [0..]
fetchRow :: SQLiteStmt -> EitherT Error IO Row
fetchRow = mapColumns fetchColumn
fetchHeaders :: SQLiteStmt -> EitherT Error IO [Header]
fetchHeaders = mapColumns fetchColumnHeader
fetchResults :: SQLite -> SQLiteStmt -> EitherT Error IO [Row]
fetchResults db stmt =
let go acc = do
status <- liftIO $ sqlite3_step stmt
case status of
_ | status == sQLITE_DONE -> return $ reverse acc
| status == sQLITE_ROW -> do
row <- fetchRow stmt
go (row:acc)
| otherwise -> left =<< liftIO (toError status db)
in go []
bindManyParameters :: SQLite -> SQLiteStmt -> [Value] -> EitherT Error IO ()
bindManyParameters db stmt params = do
expects <- liftIO $ fromIntegral <$> sqlite3_bind_parameter_count stmt
let got = length params
unless (expects == got) $ left $ ProgrammingError sQLITE_MISUSE (
"Wrong param count. Got: " ++ show got ++ " Expected: " ++ show expects)
let indices = take (length params) [1..]
binders = map (bindParameter db stmt) indices
zipWithM_ ($) binders params
executeQuery :: VaultHandle -> String -> IO (Either Error QueryResult)
{-# INLINE executeQuery #-}
executeQuery handle query = executeParameterizedQuery handle query []
stmtReadOnly :: SQLiteStmt -> IO Bool
stmtReadOnly stmt = (/= 0) <$> sqlite3_stmt_readonly stmt
rowsAffected :: SQLite -> SQLiteStmt -> IO (Maybe Int)
rowsAffected db stmt = do
readOnly <- stmtReadOnly stmt
if not readOnly
then Just . fromIntegral <$> sqlite3_changes db
else return Nothing
-- |@'fetchResults' fetch all results from a prepared
-- |@'executeParameterizedQuery' executes a SQL query, with *un-named* parameters.
--
-- @executeParameterizedQuery "SELECT * FROM table WHERE value = ?" [NullValue]
--
executeParameterizedQuery :: VaultHandle -> String -> [Value] -> IO (Either Error QueryResult)
executeParameterizedQuery handle query params = withPrim handle $ \db ->
fmap join <$> withPreparedStatement db query $ \pStmt -> runEitherT $ do
bindManyParameters db pStmt params
rows <- fetchResults db pStmt
headers <- fetchHeaders pStmt
rowsChanged <- liftIO $ rowsAffected db pStmt
lastRowId <- liftIO $ lastInsertRowId db
return QueryResult { rows = rows
, headers = headers
, lastRowId = lastRowId
, rowsChanged = rowsChanged
}
lastInsertRowId :: SQLite -> IO Int
lastInsertRowId = fmap fromIntegral . sqlite3_last_insert_rowid
-- |@'closeVault' closes a vault handle
closeVault :: VaultHandle -> IO ()
closeVault (VaultHandle ptr) = finalizeForeignPtr ptr