packages feed

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