-- Copyright (c) 2014 Sebastian Wiesner -- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the "Software"), to deal -- in the Software without restriction, including without limitation the rights -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -- copies of the Software, and to permit persons to whom the Software is -- furnished to do so, subject to the following conditions: -- The above copyright notice and this permission notice shall be included in -- all copies or substantial portions of the Software. -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -- THE SOFTWARE. {-# LANGUAGE DeriveDataTypeable #-} -- |Access to KWallet. module System.Keyring.Unix.KDE ( -- * KWallet access getPassword,setPassword -- * Errors , KWalletError(..)) where import System.Keyring.Types import Control.Exception (Exception(..),throwIO,bracket,catch) import Control.Monad (void,unless) import Data.Int (Int32) import Data.Typeable (Typeable,cast) import Network.DBus (DBusConnection,BusName(..) ,DBusCall(..),ObjectPath(..),Interface(..),Member(..) ,DBusTypeable(..),Type(..),DBusValue(..) ,DBusError(..),ErrorName(..) ,busGetSession,establish,authenticateWithRealUID,call ,returnBody,packedStringToString) import Text.Printf (printf) data KWalletError = KWalletDBusError ErrorName (Maybe String) -- ^@'KWalletDBusError' name message@ denotes an error -- received over DBus. -- -- @name@ is the proper name of the error, and @message@ is -- a human-readable error message. | KWalletOperationError String -- ^@'KWalletOperationError' message@ denotes a failed -- KWallet operation. -- -- @message@ is a human-readable error message with details -- on the error. | KWalletInvalidReturn [Type] [Type] -- ^@'KWalletInvalidReturn' expected actual@ denotes an -- unexpected return value from a DBus method call. -- -- @expected@ is the expected type signature, and @actual@ -- is the signature which was actually received from the -- remote DBus object. deriving Typeable instance Show KWalletError where show (KWalletDBusError (ErrorName name) Nothing) = "KWallet error: DBus error " ++ name show (KWalletDBusError (ErrorName name) (Just message)) = printf "KWallet error: DBus error %s: %s" name message show (KWalletOperationError message) = "KWallet error: Operation failed: " ++ message show (KWalletInvalidReturn expected actual) = printf "KWallet error: invalid return: expected %s, got %s" (show expected) (show actual) instance Exception KWalletError where toException = toException . KeyringError fromException x = do KeyringError e <- fromException x cast e throwInvalidReturn :: [Type] -> [DBusValue] -> IO a throwInvalidReturn expected actual = throwIO (KWalletInvalidReturn expected (map toSignature actual)) getKWalletKey :: AppID -> Username -> String getKWalletKey (AppID appID) (Username username) = username ++ "@" ++ appID withSessionBus :: (DBusConnection -> IO a) -> IO a withSessionBus actions = connectSession >>= actions where connectSession = establish busGetSession authenticateWithRealUID newtype AppID = AppID String instance DBusTypeable AppID where toSignature (AppID appID) = toSignature appID toDBusValue (AppID appID) = toDBusValue appID fromDBusValue value = fmap AppID (fromDBusValue value) newtype Wallet = Wallet Int32 instance DBusTypeable Wallet where toSignature (Wallet appID) = toSignature appID toDBusValue (Wallet appID) = toDBusValue appID fromDBusValue value = fmap Wallet (fromDBusValue value) callKWalletD :: DBusConnection -> String -> [DBusValue] -> IO [DBusValue] callKWalletD connection methodName args = do result <- catch (call connection busName message) (throwIO.wrapDBusError) return (returnBody result) where busName = BusName {unBusName = "org.kde.kwalletd"} path = ObjectPath { unObjectPath = "/modules/kwalletd" } interface = Interface { unInterface = "org.kde.KWallet" } member = Member { unMember = methodName } message = DBusCall { callPath = path , callInterface = Just interface , callMember = member , callBody = args} wrapDBusError DBusError{errorName=name,errorBody=body} = case body of [DBusString errMsg] -> KWalletDBusError name (Just (packedStringToString errMsg)) _ -> KWalletDBusError name Nothing openWallet :: DBusConnection -> String -> AppID -> IO Wallet openWallet connection walletName appID = do reply <- callKWalletD connection "open" [toDBusValue walletName ,DBusInt64 0 ,toDBusValue appID] case reply of [DBusInt32 handle] -> return (Wallet handle) _ -> throwInvalidReturn [SigInt32] reply closeWallet :: DBusConnection -> AppID -> Wallet -> IO () closeWallet connection appID wallet = void (callKWalletD connection "close" [toDBusValue wallet ,toDBusValue False ,toDBusValue appID]) withNetworkWallet :: AppID -> (DBusConnection -> Wallet -> IO a) -> IO a withNetworkWallet appID action = withSessionBus openNetworkWallet where openNetworkWallet connection = do reply <- callKWalletD connection "networkWallet" [] case reply of [DBusString name] -> runAction connection (packedStringToString name) _ -> throwInvalidReturn [SigString] reply runAction connection name = bracket (openWallet connection name appID) (closeWallet connection appID) (action connection) -- |@'getPassword' service username@ gets a password from the user's network -- wallet. -- -- @username@ is the name of the user whose password to get. @service@ -- identifies the application which fetches the password. -- -- This function throws 'KWalletError' if access to KWallet failed. getPassword :: Service -> Username -> IO (Maybe Password) getPassword (Service service) username = withNetworkWallet appID fetchPassword where appID = AppID service key = getKWalletKey appID username fetchPassword connection wallet = do hasPassword <- hasEntry connection wallet if hasPassword then readPassword connection wallet else return Nothing hasEntry connection wallet = do reply <- callKWalletD connection "hasEntry" [toDBusValue wallet ,toDBusValue "Passwords" ,toDBusValue key ,toDBusValue appID] case reply of [DBusBoolean b] -> return b _ -> throwInvalidReturn [SigBool] reply readPassword connection wallet = do reply <- callKWalletD connection "readPassword" [toDBusValue wallet ,toDBusValue "Passwords" ,toDBusValue key ,toDBusValue appID] case reply of [DBusString s] -> return (Just (Password (packedStringToString s))) _ -> throwInvalidReturn [SigString] reply -- |@'setPassword' service username password@ adds @password@ for @username@ to -- the user's network wallet. -- -- @username@ is the name of the user whose password to set. @service@ -- identifies the application which sets the password. -- -- This function throws 'KWalletError' if access to KWallet failed. setPassword :: Service -> Username -> Password -> IO () setPassword (Service service) username (Password password) = withNetworkWallet appID storePassword where appID = AppID service key = getKWalletKey appID username storePassword connection wallet = do folderExists <- hasFolder connection wallet unless folderExists (createFolder connection wallet) writePassword connection wallet hasFolder connection wallet = do reply <- callKWalletD connection "hasFolder" [toDBusValue wallet ,toDBusValue "Passwords" ,toDBusValue appID] case reply of [DBusBoolean b] -> return b _ -> throwInvalidReturn [SigBool] reply createFolder connection wallet = do reply <- callKWalletD connection "createFolder" [toDBusValue wallet ,toDBusValue "Passwords" ,toDBusValue appID] case reply of [DBusBoolean b] -> do unless b (throwIO (KWalletOperationError "Could not create Passwords folder")) return () _ -> throwInvalidReturn [SigBool] reply writePassword connection wallet = void $ callKWalletD connection "writePassword" [toDBusValue wallet ,toDBusValue "Passwords" ,toDBusValue key ,toDBusValue password ,toDBusValue appID]