----------------------------------------------------------------------------- -- | -- 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 4 <XMonad.Prompt> to ease password manipulation (generate, read, remove): -- -- - two to lookup passwords in the password-store; one of which copies to the -- clipboard, and the other uses @xdotool@ to type the password directly. -- -- - 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 setup through an environment variable PASSWORD_STORE_DIR, -- or @$HOME\/.password-store@ if it is unset. -- -- Source: -- -- - The password store implementation is <http://git.zx2c4.com/password-store the password-store cli>. -- -- - Inspired by <http://babushk.in/posts/combining-xmonad-and-pass.html> -- ----------------------------------------------------------------------------- module XMonad.Prompt.Pass ( -- * Usage -- $usage passPrompt , passGeneratePrompt , passRemovePrompt , passTypePrompt ) where import XMonad.Core import XMonad.Prompt ( XPrompt , showXPrompt , commandToComplete , nextCompletion , getNextCompletion , XPConfig , mkXPrompt , searchPredicate) import System.Directory (getHomeDirectory) import System.FilePath (takeExtension, dropExtension, combine) import System.Posix.Env (getEnv) import XMonad.Util.Run (runProcessWithInput) -- $usage -- 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 , xK_p) , passPrompt xpconfig) -- > , ((modMask .|. controlMask, xK_p) , passGeneratePrompt xpconfig) -- > , ((modMask .|. 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 store, see <http://git.zx2c4.com/password-store/about/> -- type Predicate = String -> String -> Bool getPassCompl :: [String] -> Predicate -> String -> IO [String] getPassCompl compls p s = return $ filter (p s) compls type PromptLabel = String newtype 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 = fmap 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 (getPassCompl passwords $ searchPredicate xpconfig) 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 -- | A prompt to type in a password for a given entry. -- This doesn't touch the clipboard. -- passTypePrompt :: XPConfig -> X () passTypePrompt = mkPassPrompt "Type password" typePassword -- | Select a password. -- selectPassword :: String -> X () selectPassword passLabel = spawn $ "pass --clip \"" ++ escapeQuote 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 \"" ++ escapeQuote passLabel ++ "\" 30" -- | Remove a password stored for a given entry. -- removePassword :: String -> X () removePassword passLabel = spawn $ "pass rm --force \"" ++ escapeQuote passLabel ++ "\"" -- | Type a password stored for a given entry using xdotool. -- typePassword :: String -> X () typePassword passLabel = spawn $ "pass \"" ++ escapeQuote passLabel ++ "\"|head -n1|tr -d '\n'|xdotool type --clearmodifiers --file -" escapeQuote :: String -> String escapeQuote = concatMap escape where escape :: Char -> String escape '"' = ['\\', '\"'] escape x = return x -- | Retrieve the list of passwords from the password store '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