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
library called 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.