module System.CredentialStore.DBusSecretService
( CredentialStore
, getCredential
, putCredential
, deleteCredential
, withCredentialStore
) where
import Control.Exception.Safe
import Control.Monad
import Crypto.Cipher.AES
import Crypto.Cipher.Types
import Crypto.Data.Padding
import Crypto.Error
import Crypto.Hash
import Crypto.KDF.HKDF
import Crypto.Number.Serialize
import Crypto.PubKey.DH
import Crypto.Random
import DBus
import DBus.Client
import Data.ByteArray
import qualified Data.ByteString as BS
import qualified Data.Map.Strict as M
data CredentialStore = CredentialStore
{ csClient :: Client
, csSession :: ObjectPath
, csCipher :: AES128
}
type CredentialObject = (ObjectPath, BS.ByteString, BS.ByteString, String)
noObject :: ObjectPath
noObject = objectPath_ "/"
destination :: BusName
destination = busName_ "org.freedesktop.secrets"
servicePath :: ObjectPath
servicePath = objectPath_ "/org/freedesktop/secrets"
serviceInterface :: InterfaceName
serviceInterface = interfaceName_ "org.freedesktop.Secret.Service"
openSession :: MemberName
openSession = memberName_ "OpenSession"
unlock :: MemberName
unlock = memberName_ "Unlock"
defaultCollection :: ObjectPath
defaultCollection = objectPath_ "/org/freedesktop/secrets/aliases/default"
collectionInterface :: InterfaceName
collectionInterface = interfaceName_ "org.freedesktop.Secret.Collection"
createItem :: MemberName
createItem = memberName_ "CreateItem"
searchItems :: MemberName
searchItems = memberName_ "SearchItems"
itemInterface :: InterfaceName
itemInterface = interfaceName_ "org.freedesktop.Secret.Item"
getSecret :: MemberName
getSecret = memberName_ "GetSecret"
delete :: MemberName
delete = memberName_ "Delete"
serviceCall :: ObjectPath -> InterfaceName -> MemberName -> MethodCall
serviceCall o i m = (methodCall o i m) { methodCallDestination = Just destination }
dhPrime :: Integer
dhPrime = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF
dhParams :: Params
dhParams = Params
{ params_p = dhPrime
, params_g = 2
, params_bits = 1024
}
withCredentialStore :: (CredentialStore -> IO a) -> IO a
withCredentialStore = bracket openStore closeStore
where
openStore = do
privateKey <- generatePrivate dhParams
let publicKey = calculatePublic dhParams privateKey
bracketOnError connectSession disconnect $ \client -> do
reply <- call_ client $
(serviceCall servicePath serviceInterface openSession)
{ methodCallBody = [ toVariant "dh-ietf1024-sha256-aes128-cbc-pkcs7", toVariant $ toVariant $ dumpKey publicKey ]
, methodCallAutoStart = True
}
case methodReturnBody reply of
[serverKeyVar, objectPathVar]
| Just v <- fromVariant objectPathVar
, Just skv <- fromVariant serverKeyVar
, Just keyDump <- fromVariant skv -> do
let serverKey = readKey keyDump
let SharedKey shared = getShared dhParams privateKey serverKey
let salt = BS.replicate (hashDigestSize (undefined :: SHA256)) 0
let prk = extract salt shared
let sessionKey = expand (prk :: PRK SHA256) BS.empty (128 `div` 8)
cipher <- throwCryptoErrorIO $ cipherInit (sessionKey :: ScrubbedBytes)
return CredentialStore
{ csClient = client
, csSession = v
, csCipher = cipher
}
body -> throw $ clientError $ "invalid OpenSession response" ++ show body
closeStore = disconnect . csClient
getCredential :: ByteArray ba => CredentialStore -> String -> IO (Maybe ba)
getCredential store@CredentialStore{..} name = do
items <- findCredentials store name
unlockReply <- call_ csClient $
(serviceCall servicePath serviceInterface unlock)
{ methodCallBody = [ toVariant items ] }
unlocked <- case methodReturnBody unlockReply of
[ unlocked, _ ] | Just objs <- fromVariant unlocked -> return objs
body -> throw $ clientError $ "invalid Unlock response" ++ show body
case unlocked of
[] -> return Nothing
(objpath : _) -> do
getReply <- call_ csClient $
(serviceCall objpath itemInterface getSecret)
{ methodCallBody = [ toVariant csSession ] }
case methodReturnBody getReply of
[ obj ] | Just co <- fromVariant obj ->
fmap Just $ decryptCredential csCipher (credParam co) (credData co)
body -> throw $ clientError $ "invalid GetSecret response" ++ show body
where
credData :: CredentialObject -> BS.ByteString
credData (_, _, v, _) = v
credParam (_, p, _, _) = p
putCredential :: ByteArray ba => CredentialStore -> String -> ba -> IO ()
putCredential CredentialStore{..} name value = do
(cred, iv) <- encryptCredential csCipher value
reply <- call_ csClient $
(serviceCall defaultCollection collectionInterface createItem)
{ methodCallBody =
[ toVariant $ M.fromList
[ ("org.freedesktop.Secret.Item.Label", toVariant name)
, ("org.freedesktop.Secret.Item.Attributes",
toVariant $ M.singleton "credentialName" name)
]
, toVariant
( csSession
, iv
, cred
, "text/plain; charset=utf8"
)
, toVariant True
]
}
case methodReturnBody reply of
[ path, _ ] | Just p <- fromVariant path -> when (p == noObject) $ throw (clientError "prompt required")
body -> throw $ clientError $ "invalid CreateItem response" ++ show body
deleteCredential :: CredentialStore -> String -> IO ()
deleteCredential store@CredentialStore{..} name = do
items <- findCredentials store name
forM_ items $ \objpath ->
call_ csClient $ serviceCall objpath itemInterface delete
findCredentials :: CredentialStore -> String -> IO [ObjectPath]
findCredentials CredentialStore{..} name = do
searchReply <- call_ csClient $
(serviceCall defaultCollection collectionInterface searchItems)
{ methodCallBody = [ toVariant $ M.singleton "credentialName" name ] }
case methodReturnBody searchReply of
[ v ] | Just items <- fromVariant v -> return items
body -> throw $ clientError $ "invalid SearchItems response" ++ show body
decryptCredential :: (BlockCipher c, ByteArray ba) => c -> BS.ByteString -> BS.ByteString -> IO ba
decryptCredential cipher ivbytes bs = do
iv <- case makeIV ivbytes of
Just iv -> return iv
Nothing -> throw $ clientError $ "invalid credential IV"
let decrypted = cbcDecrypt cipher iv $ convert bs
case unpad (PKCS7 $ blockSize cipher) decrypted of
Nothing -> throw $ clientError $ "invalid decrypred credential"
Just cred -> return cred
encryptCredential :: (BlockCipher c, ByteArray ba) => c -> ba -> IO (BS.ByteString, BS.ByteString)
encryptCredential cipher ba = do
let padded = pad (PKCS7 $ blockSize cipher) ba
ivbytes <- getRandomBytes (blockSize cipher)
let Just iv = makeIV ivbytes
let encrypted = cbcEncrypt cipher iv padded
return (convert encrypted, ivbytes)
dumpKey :: PublicNumber -> BS.ByteString
dumpKey (PublicNumber key) = i2osp key
readKey :: BS.ByteString -> PublicNumber
readKey = PublicNumber . os2ip