----------------------------------------------------------------------------- -- | -- Module : XMonad.Prompt.Pass -- Description : A prompt for interacting with @pass(1)@. -- Copyright : (c) 2014 Igor Babuschkin, Antoine R. Dumont -- License : BSD3-style (see LICENSE) -- -- Maintainer : Antoine R. Dumont -- Stability : unstable -- Portability : unportable -- -- A thin wrapper around the standard @pass(1)@ UNIX utility. -- -- This module provides several prompts to ease password manipulation -- (generate, read, edit, remove); all of them benefit from the -- completion system provided by "XMonad.Prompt". Specifically, we -- provide -- -- - two functions to lookup passwords in the password-store: -- -- - 'passPrompt' copies the password directly to the clipboard. -- -- - 'passTypePrompt' uses @xdotool@ to type the password -- directly. -- -- - 'passGeneratePrompt' generates a password for a given password -- label that the user inputs. -- -- - 'passEditPrompt' edits a password for a given password label that -- the user inputs. -- -- - 'passRemovePrompt' deletes a stored password for a given password -- label that the user inputs. -- -- The password store is setup through an environment variable -- @$PASSWORD_STORE_DIR@, or @$HOME\/.password-store@ if it is unset. -- The editor is determined from the environment variable @$EDITOR@. -- -- Source: -- -- - The -- implementation is . -- -- - Inspired by -- ----------------------------------------------------------------------------- module XMonad.Prompt.Pass ( -- * Usage -- $usage -- * Retrieving passwords passPrompt , passPrompt' , passTypePrompt -- * Editing passwords , passEditPrompt , passEditPrompt' , passRemovePrompt , passRemovePrompt' , passGeneratePrompt , passGeneratePrompt' , passGenerateAndCopyPrompt , passGenerateAndCopyPrompt' -- * Misc , passOTPPrompt ) where import System.Directory (getHomeDirectory) import System.FilePath (combine, dropExtension, takeExtension) import System.Posix.Env (getEnv) import XMonad.Core import XMonad.Prompt ( XPrompt , showXPrompt , commandToComplete , nextCompletion , getNextCompletion , XPConfig , mkXPrompt , searchPredicate) 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', -- 'passRemovePrompt', 'passEditPrompt' or 'passTypePrompt': -- -- > , ((modMask , xK_p) , passPrompt def) -- > , ((modMask .|. controlMask, xK_p) , passGeneratePrompt def) -- > , ((modMask .|. shiftMask, xK_p) , passEditPrompt def) -- > , ((modMask .|. controlMask .|. shiftMask, xK_p), passRemovePrompt def) -- -- You can also use the versions that let you specify a custom prompt: -- -- > , ((modMask , xK_p) , passPrompt' "Ask 'pass' for" def) -- -- Note that, by default, we do not use fuzzy matching in this module. -- To enable this feature, import the "XMonad.Prompt.FuzzyMatch" module -- and add the relevant functions to your prompt configuration: -- -- > myXPConfig :: XPConfig -- > myXPConfig = def -- > { searchPredicate = fuzzyMatch -- > , sorter = fuzzySort -- > } -- > -- > , ((modMask , xK_p), passPrompt myXPConfig) -- -- For detailed instructions on: -- -- - editing your key bindings, see "XMonad.Doc.Extending#Editing_key_bindings". -- -- - how to setup the password store, see -- or @man 1 pass@. -- 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 = passPrompt' "Select password" -- | The same as 'passPrompt' but with a user-specified prompt. passPrompt' :: String -> XPConfig -> X () passPrompt' s = mkPassPrompt s selectPassword -- | A prompt to retrieve a OTP from a given entry. Note that you will -- need to use the -- extension for this to work. -- passOTPPrompt :: XPConfig -> X () passOTPPrompt = mkPassPrompt "Select OTP" selectOTP -- | 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 = passGeneratePrompt' "Generate password" -- | The same as 'passGeneratePrompt' but with a user-specified prompt. passGeneratePrompt' :: String -> XPConfig -> X () passGeneratePrompt' s = mkPassPrompt s generatePassword -- | A prompt to generate a password for a given entry and immediately copy it -- to the clipboard. This can be used to override an already stored entry. -- (Beware that no confirmation is asked) -- passGenerateAndCopyPrompt :: XPConfig -> X () passGenerateAndCopyPrompt = passGenerateAndCopyPrompt' "Generate and copy password" -- | The same as 'passGenerateAndCopyPrompt' but with a user-specified prompt. passGenerateAndCopyPrompt' :: String -> XPConfig -> X () passGenerateAndCopyPrompt' s = mkPassPrompt s generateAndCopyPassword -- | A prompt to remove a password for a given entry. -- (Beware that no confirmation is asked) -- passRemovePrompt :: XPConfig -> X () passRemovePrompt = passRemovePrompt' "Remove password" -- | The same as 'passRemovePrompt' but with a user-specified prompt. passRemovePrompt' :: String -> XPConfig -> X () passRemovePrompt' s = mkPassPrompt s 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 -- | A prompt to edit a given entry. -- This doesn't touch the clipboard. -- passEditPrompt :: XPConfig -> X () passEditPrompt = passEditPrompt' "Edit password" -- | The same as 'passEditPrompt' but with a user-specified prompt. passEditPrompt' :: String -> XPConfig -> X () passEditPrompt' s = mkPassPrompt s editPassword -- | Select a password. -- selectPassword :: String -> X () selectPassword passLabel = spawn $ "pass --clip \"" ++ escapeQuote passLabel ++ "\"" -- | Select an OTP. -- selectOTP :: String -> X () selectOTP passLabel = spawn $ "pass otp --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" -- | Generate a 30 characters password for a given entry. -- If the entry already exists, it is updated with a new password. -- After generating the password, it is copied to the clipboard. -- generateAndCopyPassword :: String -> X () generateAndCopyPassword passLabel = spawn $ "pass generate --force -c \"" ++ escapeQuote passLabel ++ "\" 30" -- | Remove a password stored for a given entry. -- removePassword :: String -> X () removePassword passLabel = spawn $ "pass rm --force \"" ++ escapeQuote passLabel ++ "\"" -- | Edit a password stored for a given entry. -- editPassword :: String -> X () editPassword passLabel = spawn $ "pass edit \"" ++ 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 = [x] -- | Retrieve the list of passwords from the password store 'passwordStoreDir' -- getPasswords :: FilePath -> IO [String] getPasswords passwordStoreDir = do files <- runProcessWithInput "find" [ "-L", -- Traverse symlinks 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