-----------------------------------------------------------------------------
-- |
-- Module : XMonad.Prompt.Pass
-- Copyright : (c) 2014 Igor Babuschkin, Antoine R. Dumont
-- License : BSD3-style (see LICENSE)
--
-- Maintainer : Antoine R. Dumont <eniotna.t@gmail.com>
-- Stability : unstable
-- Portability : unportable
--
-- This module provides 3 <XMonad.Prompt> to ease passwords manipulation (generate, read, remove):
--
-- - one to lookup passwords in the password-storage.
--
-- - one to generate a password for a given password label that the user inputs.
--
-- - one to delete a stored password for a given password label that the user inputs.
--
-- All those prompts benefit from the completion system provided by the module <XMonad.Prompt>.
--
-- The password store is setuped through an environment variable PASSWORD_STORE_DIR.
-- If this is set, use the content of the variable.
-- Otherwise, the password store is located on user's home @$HOME\/.password-store@.
--
--
-- Source:
--
-- - The password storage implementation is <http://git.zx2c4.com/password-store the password-store cli>.
--
-- - Inspired from <http://babushk.in/posts/combining-xmonad-and-pass.html>
--
-----------------------------------------------------------------------------
module XMonad.Prompt.Pass (
-- * Usages
-- $usages
passPrompt
, passGeneratePrompt
, passRemovePrompt
) where
import Control.Monad (liftM)
import XMonad.Core
import XMonad.Prompt ( XPrompt
, showXPrompt
, commandToComplete
, nextCompletion
, getNextCompletion
, XPConfig
, mkXPrompt
, mkComplFunFromList)
import System.Directory (getHomeDirectory)
import System.FilePath (takeExtension, dropExtension, combine)
import System.Posix.Env (getEnv)
import XMonad.Util.Run (runProcessWithInput)
-- $usages
-- You can use this module with the following in your @~\/.xmonad\/xmonad.hs@:
--
-- > import XMonad.Prompt.Pass
--
-- Then add a keybinding for 'passPrompt', 'passGeneratePrompt' or 'passRemovePrompt':
--
-- > , ((modMask x , xK_p) , passPrompt xpconfig)
-- > , ((modMask x .|. controlMask, xK_p) , passGeneratePrompt xpconfig)
-- > , ((modMask x .|. controlMask .|. shiftMask, xK_p), passRemovePrompt xpconfig)
--
-- For detailed instructions on:
--
-- - editing your key bindings, see "XMonad.Doc.Extending#Editing_key_bindings".
--
-- - how to setup the password storage, see <http://git.zx2c4.com/password-store/about/>
--
type PromptLabel = String
data Pass = Pass PromptLabel
instance XPrompt Pass where
showXPrompt (Pass prompt) = prompt ++ ": "
commandToComplete _ c = c
nextCompletion _ = getNextCompletion
-- | Default password store folder in $HOME/.password-store
--
passwordStoreFolderDefault :: String -> String
passwordStoreFolderDefault home = combine home ".password-store"
-- | Compute the password store's location.
-- Use the PASSWORD_STORE_DIR environment variable to set the password store.
-- If empty, return the password store located in user's home.
--
passwordStoreFolder :: IO String
passwordStoreFolder =
getEnv "PASSWORD_STORE_DIR" >>= computePasswordStoreDir
where computePasswordStoreDir Nothing = liftM passwordStoreFolderDefault getHomeDirectory
computePasswordStoreDir (Just storeDir) = return storeDir
-- | A pass prompt factory
--
mkPassPrompt :: PromptLabel -> (String -> X ()) -> XPConfig -> X ()
mkPassPrompt promptLabel passwordFunction xpconfig = do
passwords <- io (passwordStoreFolder >>= getPasswords)
mkXPrompt (Pass promptLabel) xpconfig (mkComplFunFromList passwords) passwordFunction
-- | A prompt to retrieve a password from a given entry.
--
passPrompt :: XPConfig -> X ()
passPrompt = mkPassPrompt "Select password" selectPassword
-- | A prompt to generate a password for a given entry.
-- This can be used to override an already stored entry.
-- (Beware that no confirmation is asked)
--
passGeneratePrompt :: XPConfig -> X ()
passGeneratePrompt = mkPassPrompt "Generate password" generatePassword
-- | A prompt to remove a password for a given entry.
-- (Beware that no confirmation is asked)
--
passRemovePrompt :: XPConfig -> X ()
passRemovePrompt = mkPassPrompt "Remove password" removePassword
-- | Select a password.
--
selectPassword :: String -> X ()
selectPassword passLabel = spawn $ "pass --clip " ++ passLabel
-- | Generate a 30 characters password for a given entry.
-- If the entry already exists, it is updated with a new password.
--
generatePassword :: String -> X ()
generatePassword passLabel = spawn $ "pass generate --force " ++ passLabel ++ " 30"
-- | Remove a password stored for a given entry.
--
removePassword :: String -> X ()
removePassword passLabel = spawn $ "pass rm --force " ++ passLabel
-- | Retrieve the list of passwords from the password storage 'passwordStoreDir
getPasswords :: FilePath -> IO [String]
getPasswords passwordStoreDir = do
files <- runProcessWithInput "find" [
passwordStoreDir,
"-type", "f",
"-name", "*.gpg",
"-printf", "%P\n"] []
return $ map removeGpgExtension $ lines files
removeGpgExtension :: String -> String
removeGpgExtension file | takeExtension file == ".gpg" = dropExtension file
| otherwise = file