For the past few months, I have been enjoyably using the pass password manager. It is a simple commandline tool that bundles together a variety of unix utilities into a nice password manager. The passwords are stored in GPG encrypted files, organized using the filesystem. They can be synced using remote git repos. It is all quite elegant and easy to customize using the underlying tools (I use a USB thumb drive as the remote repository).
The part that it lacked, though, was the ease of retrieving a password ala something
like 1Password or LastPass. Having to tab over to a shell to run the command and
then tab back to the window that required the password was a bit tiring (yeah I know,
first world problems :P). So I decided to try and build the functionality into
XMonad, my window manager of choice. XMonad comes with a really handy utility
Prompt which I already use for a fast process launcher. The
challenge was to use the underlying stuff to make a custom prompt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 getFiles :: String -> IO [String] getFiles dir = do names <- getDirectoryContents dir let properNames = filter (`notElem` [ ".", "..", ".git" ]) names paths <- forM properNames $ \name -> do let path = dir </> name isDirectory <- doesDirectoryExist path if isDirectory then getFiles path else return [path] return (concat paths) getPasswords :: IO [String] getPasswords = do dir <- getEnv "PASSWORD_STORE_DIR" let password_dir = dir </> "" files <- getFiles password_dir return $ map ((makeRelative password_dir) . dropExtension) files
I started off referencing an existing implementation that uses a third party
library. I dislike using third party libraries if I can, as it adds complexity
(especially with haskell, as
cabal is kind of stinky to use). It also seems a bit
ridiculous to think that directory traversing is so difficult in the language to
require a library. So I found a tutorial that gives an example of how to
generate a list of the files in a directory, which works great. The only thing I
changed was to add
.git as an exempt path (no need to complete the git
meta data). Then I wrapped the function with another that handles
the password store specific logic, like finding the directory and stripping off the
file structure extras, and outputs the clean password list.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 data Pass = Pass instance XPrompt Pass where showXPrompt Pass = "Pass: " commandToComplete _ c = c nextCompletion _ = getNextCompletion selectPassword :: String -> X () selectPassword ps = spawn $ "pass -c " ++ ps passwordPrompt :: XPConfig -> X () passwordPrompt config = do li <- io getPasswords mkXPrompt Pass config (mkComplFunFromList li) selectPassword
Then all that was left was using some of the utility functions to turn this list into a completion function and seed the custom prompt with it.