From 085eb285bd1ac09d3f9b46cac646aba2b0d58fa3 Mon Sep 17 00:00:00 2001 From: greg Date: Thu, 3 Oct 2013 16:52:59 +0200 Subject: [PATCH 01/15] Allow passing parameters to GPG --- README.md | 1 + bash_completion.d/pwsafe | 2 +- src/Main.hs | 2 +- src/Options.hs | 3 +++ src/Run.hs | 4 ++-- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7a52929..200313a 100644 --- a/README.md +++ b/README.md @@ -141,3 +141,4 @@ Command line options this option is to be used with --add -n NUMBER copy password n times to clipboard; defaults to 1 + -g GPG Option add a GPG option (repeat to add multiple) diff --git a/bash_completion.d/pwsafe b/bash_completion.d/pwsafe index 8fc5738..18168a5 100644 --- a/bash_completion.d/pwsafe +++ b/bash_completion.d/pwsafe @@ -9,7 +9,7 @@ _pwsafe() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - opts='--help -a --add -q --query -l --list -e --edit --dump --lock --unlock --dbfile --user --password-only' + opts='--help -a --add -q --query -l --list -e --edit --dump --lock --unlock --dbfile --user --password-only -g' case "${prev}" in diff --git a/src/Main.hs b/src/Main.hs index 3412377..b781d7e 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -10,4 +10,4 @@ import Run (run) main :: IO () main = do args <- getArgs - run defaultConfig (Cipher.gpgCipher []) stdout args + run defaultConfig (Cipher.gpgCipher) stdout args diff --git a/src/Options.hs b/src/Options.hs index 7a47407..482d069 100644 --- a/src/Options.hs +++ b/src/Options.hs @@ -16,6 +16,7 @@ data Options = Options { , userName :: Maybe String , repeatCount :: Maybe Int , passwordOnly :: Bool + , gpgOptions :: [String] } deriving Show defaultOptions :: Options @@ -25,6 +26,7 @@ defaultOptions = Options { , userName = Nothing , repeatCount = Nothing , passwordOnly = False + , gpgOptions = [] } options :: [OptDescr (Options -> Options)] @@ -43,6 +45,7 @@ options = [ , Option ['n'] [] (ReqArg (\s opts -> opts { repeatCount = (Just . read) s }) "NUMBER") "copy password n times to clipboard;\ndefaults to 1" , Option [] ["password-only"] (NoArg (\ opts -> opts { passwordOnly = True})) "only copy password to clipboard" + , Option ['g'] [] (ReqArg (\s opts -> opts { gpgOptions = (gpgOptions opts) ++ [s]}) "GPG Option") "add a GPG option (repeat to add multiple)" ] defaultDatabaseFile :: IO String diff --git a/src/Run.hs b/src/Run.hs index 07b85d1..49f8044 100644 --- a/src/Run.hs +++ b/src/Run.hs @@ -11,10 +11,10 @@ import Config (Config) import qualified Action import Cipher (Cipher) -run :: Config -> (FilePath -> Cipher) -> Handle -> [String] -> IO () +run :: Config -> ([String] -> FilePath -> Cipher) -> Handle -> [String] -> IO () run conf cipher h args = do opts <- Options.get args - let c = cipher $ Options.databaseFile opts + let c = cipher (Options.gpgOptions opts) (Options.databaseFile opts) runAction = Action.runAction (Action.mkEnv conf c h) case Options.mode opts of Help -> Options.printHelp From 65e714b57fddaf380365e05d775ffff9fb276861 Mon Sep 17 00:00:00 2001 From: greg Date: Sat, 5 Oct 2013 19:33:27 +0200 Subject: [PATCH 02/15] FreeBSD support --- src/Lock.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Lock.hs b/src/Lock.hs index 9b18df0..1747e97 100644 --- a/src/Lock.hs +++ b/src/Lock.hs @@ -7,13 +7,13 @@ import qualified Control.Exception as E -- | Try to acquire the lock, return `True` on success acquire :: IO Bool -acquire = (semOpen "pwsafe" (OpenSemFlags True True) readWriteMode 0 >> return True) `E.catch` handlerFalse +acquire = (semOpen "/pwsafe" (OpenSemFlags True True) readWriteMode 0 >> return True) `E.catch` handlerFalse where readWriteMode = ownerReadMode `unionFileModes` ownerWriteMode -- | Release the lock, return `True` on success release :: IO Bool -release = (semUnlink "pwsafe" >> return True) `E.catch` handlerFalse +release = (semUnlink "/pwsafe" >> return True) `E.catch` handlerFalse -- | Return `False` on `E.SomeException` handlerFalse :: E.SomeException -> IO Bool From bdffbed3d3114f4432e90a64ebde536ee6190d61 Mon Sep 17 00:00:00 2001 From: greg Date: Thu, 3 Oct 2013 18:35:38 +0200 Subject: [PATCH 03/15] Improved error handling when running external commands --- src/Config.hs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Config.hs b/src/Config.hs index 108e64f..d9f918e 100644 --- a/src/Config.hs +++ b/src/Config.hs @@ -2,6 +2,9 @@ module Config (Config(..), defaultConfig) where import System.Process import Util (run) +import Control.Exception +import System.Exit +import System.IO data Config = Config { copyToClipboard :: String -> IO () @@ -18,16 +21,24 @@ defaultConfig = Config { , generatePassword = pwgen (Just 20) } +readProcess' :: FilePath -> [String] -> String -> IO String +readProcess' fp a i = handle handler $ readProcess fp a i + where + handler e = do + let err = show (e :: SomeException) + hPutStr stderr $ "Cannot execute " ++ fp ++ ". Error was: " ++ err + exitWith (ExitFailure 1) + xclip :: String -> IO () -- vimperator, for some reason, needs -l 2, pentadactyl works with -l 1 -- xclip input = readProcess "xclip" ["-l", "2", "-quiet"] input >> return () -xclip input = readProcess "xclip" ["-l", "1", "-quiet"] input >> return () +xclip input = readProcess' "xclip" ["-l", "1", "-quiet"] input >> return () xdgOpen :: String -> IO () xdgOpen url = run "xdg-open" [url] pwgen :: Maybe Int -- ^ length of generated password -> IO String -pwgen mLength = fmap init $ readProcess "pwgen" args "" +pwgen mLength = fmap init $ readProcess' "pwgen" args "" where args = "-s" : maybe [] (return . show) mLength From 364cd0f95041846c848e5a06fd9399ca4cbb3cad Mon Sep 17 00:00:00 2001 From: greg Date: Sat, 5 Oct 2013 19:55:51 +0200 Subject: [PATCH 04/15] Make bash completion script more portable --- bash_completion.d/pwsafe | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bash_completion.d/pwsafe b/bash_completion.d/pwsafe index 18168a5..b9b5f82 100644 --- a/bash_completion.d/pwsafe +++ b/bash_completion.d/pwsafe @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # put into /etc/bash_completion.d/ # From 995474b4d46f836d783f42ed892f342eb2e75219 Mon Sep 17 00:00:00 2001 From: greg Date: Sat, 5 Oct 2013 19:48:01 +0200 Subject: [PATCH 05/15] Updated documentation --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 200313a..dd9bea0 100644 --- a/README.md +++ b/README.md @@ -141,4 +141,5 @@ Command line options this option is to be used with --add -n NUMBER copy password n times to clipboard; defaults to 1 + --password-only only copy password to clipboard -g GPG Option add a GPG option (repeat to add multiple) From 84303c9e6eca5d87dd0536130cafa919da01386c Mon Sep 17 00:00:00 2001 From: greg Date: Thu, 3 Oct 2013 19:09:48 +0200 Subject: [PATCH 06/15] Added the -d option to prevent opening a browser when querying the DB --- README.md | 5 ++++- bash_completion.d/pwsafe | 2 +- src/Action.hs | 6 +++--- src/Options.hs | 3 +++ src/Run.hs | 2 +- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dd9bea0..7d17a5e 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,8 @@ If you want to log in to niftyservice again, proceed as follows. At this point, pwsafe has opened the website that corresponds to the entry in your default web browser. To configure your default web browser, consult - the documentation of xdg-open and update-alternatives. + the documentation of xdg-open and update-alternatives. If you do not want + pwsafe to open your browser, please supply the -d option. 2. Switch to your browser window and navigate to the login page. @@ -143,3 +144,5 @@ Command line options defaults to 1 --password-only only copy password to clipboard -g GPG Option add a GPG option (repeat to add multiple) + -d don't open a browser when using -q + diff --git a/bash_completion.d/pwsafe b/bash_completion.d/pwsafe index b9b5f82..91abccf 100644 --- a/bash_completion.d/pwsafe +++ b/bash_completion.d/pwsafe @@ -9,7 +9,7 @@ _pwsafe() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - opts='--help -a --add -q --query -l --list -e --edit --dump --lock --unlock --dbfile --user --password-only -g' + opts='--help -a --add -q --query -l --list -e --edit --dump --lock --unlock --dbfile --user --password-only -g -d' case "${prev}" in diff --git a/src/Action.hs b/src/Action.hs index 08c78c0..466c0c8 100644 --- a/src/Action.hs +++ b/src/Action.hs @@ -92,8 +92,8 @@ add url_ mUser = do Left err -> error err Right db_ -> saveDatabase db_ -query :: String -> Int -> Bool -> ActionM () -query kw n passwordOnly = do +query :: String -> Int -> Bool -> Bool -> ActionM () +query kw n passwordOnly noOpen = do db <- openDatabase case Database.lookupEntry db kw of Left err -> putStrLn err @@ -101,7 +101,7 @@ query kw n passwordOnly = do unless passwordOnly $ do forM_ (entryUrl x) $ \url -> do putStrLn url - open url + unless noOpen $ open url forM_ (entryUser x) copyToClipboard forM_ (entryPassword x) $ diff --git a/src/Options.hs b/src/Options.hs index 482d069..cf010e6 100644 --- a/src/Options.hs +++ b/src/Options.hs @@ -17,6 +17,7 @@ data Options = Options { , repeatCount :: Maybe Int , passwordOnly :: Bool , gpgOptions :: [String] + , dontOpen :: Bool } deriving Show defaultOptions :: Options @@ -27,6 +28,7 @@ defaultOptions = Options { , repeatCount = Nothing , passwordOnly = False , gpgOptions = [] + , dontOpen = False } options :: [OptDescr (Options -> Options)] @@ -46,6 +48,7 @@ options = [ , Option [] ["password-only"] (NoArg (\ opts -> opts { passwordOnly = True})) "only copy password to clipboard" , Option ['g'] [] (ReqArg (\s opts -> opts { gpgOptions = (gpgOptions opts) ++ [s]}) "GPG Option") "add a GPG option (repeat to add multiple)" + , Option ['d'] [] (NoArg (\ opts -> opts { dontOpen = True })) "don't open a browser when using -q" ] defaultDatabaseFile :: IO String diff --git a/src/Run.hs b/src/Run.hs index 49f8044..6c02b5f 100644 --- a/src/Run.hs +++ b/src/Run.hs @@ -19,7 +19,7 @@ run conf cipher h args = do case Options.mode opts of Help -> Options.printHelp Add url -> withLock $ runAction $ Action.add url (Options.userName opts) - Query s -> runAction $ Action.query s (maybe 1 id $ Options.repeatCount opts) (Options.passwordOnly opts) + Query s -> runAction $ Action.query s (maybe 1 id $ Options.repeatCount opts) (Options.passwordOnly opts) (Options.dontOpen opts) List p -> runAction $ Action.list p Edit -> withLock $ Action.edit c Dump -> runAction $ Action.dump From fed9bb7ad49bae33ab10217487e9402bfc8b21e5 Mon Sep 17 00:00:00 2001 From: greg Date: Fri, 4 Oct 2013 11:09:20 +0200 Subject: [PATCH 07/15] Initial support for multiple accounts per service --- README.md | 29 ++++++++++++---- pwsafe.cabal | 1 + src/Action.hs | 26 +++++++++----- src/Database.hs | 90 ++++++++++++++++++++++++++++++++++++------------- src/Options.hs | 5 ++- src/Run.hs | 4 +-- 6 files changed, 113 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 7d17a5e..40d9a5b 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,18 @@ If you want to log in to niftyservice again, proceed as follows. 5. pwsafe exits, and you can proceed with the login. +Using multiple accounts per service +----------------------------------- + +pwsafe allows you to add more than just one user account per service. In order +to do so, you have to supply the -A option when adding a second user account. +When you query the database, pwsafe will either copy the credentials into the +paste buffer (if there is only one account for the respective service) or write +a list of known usernames to stdout and exit with code 2. You can supply the +--user parameter when querying, so that pwsafe knows which entry in the database +you would like to have. + + Changing or deleting an entry in the database --------------------------------------------- @@ -112,11 +124,14 @@ password. user=rwDJEs5J password=XArG9R4QBDwR7ceCVjyV url=http://www.niftyservice.com + user_1=another_user + password=1=BLABLA + url=http://www.niftyservice.com -Only the "name" and "password" fields are required. "user" and "url" are -optional. If the url field is not present, pwsafe will not invoke your web -browser on query. If the user field is not present, pwsafe will only provide -the password to paste on query. +Only the "name" (i.e., section name) and "password" fields are required. "user" +and "url" are optional. If the url field is not present, pwsafe will not invoke +your web browser on query. If the user field is not present, pwsafe will only +provide the password to paste on query. Command line options @@ -138,11 +153,11 @@ Command line options --unlock release write lock for database --dbfile=FILE file where passwords are stored; defaults to ~/.pwsafe/db - --user=USER specify a username to be used for a new entry; - this option is to be used with --add + --user=USER specify a username to be used for an entry; + this option is to be used with --add or -q -n NUMBER copy password n times to clipboard; defaults to 1 --password-only only copy password to clipboard -g GPG Option add a GPG option (repeat to add multiple) -d don't open a browser when using -q - + -A force add a new account diff --git a/pwsafe.cabal b/pwsafe.cabal index f21613f..41089ec 100644 --- a/pwsafe.cabal +++ b/pwsafe.cabal @@ -23,6 +23,7 @@ executable pwsafe , deepseq , unix , config-ng + , MissingH main-is: Main.hs diff --git a/src/Action.hs b/src/Action.hs index 466c0c8..0ae9f93 100644 --- a/src/Action.hs +++ b/src/Action.hs @@ -2,14 +2,17 @@ module Action (runAction, mkEnv, add, query, list, edit, dump) where import Prelude hiding (putStrLn, putStr) +import qualified Prelude import Control.Monad (replicateM_, unless) import Control.Monad.Trans.Reader import Control.Monad.IO.Class (liftIO) import System.IO (hPutStr, hClose) import qualified System.IO as IO +import System.Exit import Control.DeepSeq (deepseq) import Text.Printf import Data.Foldable (forM_) +import Data.Maybe (mapMaybe) import Util (nameFromUrl, run, withTempFile, match_) import Database (Database, Entry(..)) @@ -38,6 +41,10 @@ mkEnv conf cipher h = Env { , envHandle = h } +doIO :: IO a -> ActionM a +doIO action = ActionM $ do + liftIO action + liftAction :: (Env -> IO a) -> ActionM a liftAction action = ActionM $ do env <- ask @@ -70,8 +77,8 @@ saveDatabase :: Database -> ActionM () saveDatabase = encrypt . Database.render -add :: String -> Maybe String -> ActionM () -add url_ mUser = do +add :: String -> Maybe String -> Bool -> ActionM () +add url_ mUser force = do user <- maybe genUser return mUser password_ <- genPassword addEntry $ Entry {entryName = nameFromUrl url_, entryUser = Just user, entryPassword = Just password_, entryUrl = Just url_} @@ -88,16 +95,16 @@ add url_ mUser = do -- gets an error before he has to enter his password. entry `deepseq` do db <- openDatabase - case Database.addEntry db entry of + case Database.addEntry db entry force of Left err -> error err Right db_ -> saveDatabase db_ -query :: String -> Int -> Bool -> Bool -> ActionM () -query kw n passwordOnly noOpen = do +query :: String -> Maybe String -> Int -> Bool -> Bool -> ActionM () +query kw mUser n passwordOnly noOpen = do db <- openDatabase - case Database.lookupEntry db kw of - Left err -> putStrLn err - Right x -> x `deepseq` do -- force pending exceptions early.. + case Database.lookupEntryUser db kw mUser of + Left err -> putStrLn err >> (doIO $ exitWith (ExitFailure 1)) + Right (x:[]) -> x `deepseq` do -- force pending exceptions early.. unless passwordOnly $ do forM_ (entryUrl x) $ \url -> do putStrLn url @@ -106,6 +113,9 @@ query kw n passwordOnly noOpen = do copyToClipboard forM_ (entryPassword x) $ replicateM_ n . copyToClipboard + Right xs -> do + mapM_ putStrLn (mapMaybe entryUser xs) + doIO $ exitWith (ExitFailure 2) where open = liftAction1 (Config.openUrl . envConfig) diff --git a/src/Database.hs b/src/Database.hs index 7a6e60d..2f6e85f 100644 --- a/src/Database.hs +++ b/src/Database.hs @@ -1,8 +1,10 @@ -module Database (Database, empty, parse, render, addEntry, Entry(..), lookupEntry, hasEntry, entryNames) where +module Database (Database, empty, parse, render, addEntry, Entry(..), lookupEntryUser, hasEntry, entryNames) where import Prelude hiding (lookup) -import Data.List (intercalate) +import Data.List (intercalate, sort) +import Data.String.Utils +import Data.Maybe import Control.DeepSeq import Text.Printf (printf) @@ -26,20 +28,55 @@ newtype Database = Database { config :: Config } empty :: Database empty = Database Config.empty -lookupEntry :: Database -> String -> Either String Entry +lookupEntry :: Database -> String -> Either String [Entry] lookupEntry db s = case match s $ entryNames db of None -> Left "no match" - Ambiguous l -> Left $ printf "ambiguous, could refer to:\n %s" $ intercalate "\n " l - Match name -> entry + Ambiguous l -> Left $ printf "ambiguous, could refer to:\n %s" $ intercalate "\n " l + Match name -> Right $ getEntries db name + +lookupEntryUser :: Database -> String -> Maybe String -> Either String [Entry] +lookupEntryUser db s mUser = + case entries of + Left _ -> entries + Right l -> Right $ maybe l (maybeToList . findUser l) mUser + where entries = lookupEntry db s + +getEntries :: Database -> String -> [Entry] +getEntries db name = + map makeEntry $ zip3 users passwords urls where - entry = do - return Entry { - entryName = name - , entryUser = lookup "user" - , entryPassword = lookup "password" - , entryUrl = lookup "url" - } - lookup k = Config.lookup name k (config db) + knownKeys = sort $ Config.keys name (config db) + isKey k x = (x == k || startswith (k ++ "_") x) + userSuffixes = map (drop 4) $ filter (isKey "user") knownKeys + users = map (lookup . ("user"++)) userSuffixes + passwords = map (lookup . ("password"++)) userSuffixes + urls = map (lookup . ("url"++)) userSuffixes + lookup k = Config.lookup name k (config db) + + makeEntry (us, pw, ur) = Entry { + entryName = name + , entryUser = us + , entryPassword = pw + , entryUrl = ur + } + +buildEntrySuffix :: Database -> String -> String +buildEntrySuffix db name = + newSuffixes !! 0 + where + knownKeys = sort $ Config.keys name (config db) + isKey k x = (x == k || startswith (k ++ "_") x) + userSuffixes = map (drop 4) $ filter (isKey "user") knownKeys + passSuffixes = map (drop 8) $ filter (isKey "password") knownKeys + urlSuffixes = map (drop 3) $ filter (isKey "url") knownKeys + suffixes = userSuffixes ++ passSuffixes ++ urlSuffixes + newSuffixes = dropWhile (`elem` suffixes) ("":map (("_" ++) . show) [1..]) + +findUser :: [Entry] -> String -> Maybe Entry +findUser [] _ = Nothing +findUser (x:xs) u = if (entryUser x) == Just u + then Just x + else findUser xs u hasEntry :: String -> Database -> Bool hasEntry name = Config.hasSection name . config @@ -47,19 +84,24 @@ hasEntry name = Config.hasSection name . config entryNames :: Database -> [String] entryNames = Config.sections . config -addEntry :: Database -> Entry -> Either String Database -addEntry db entry = +addEntry :: Database -> Entry -> Bool -> Either String Database +addEntry db entry forceAdd = case hasEntry name db of - True -> Left $ printf "Entry with name \"%s\" already exists!" name - False -> Right db {config = insertEntry entry $ config db} + True -> + if forceAdd && not (isJust $ findUser (getEntries db name) (fromJust $ entryUser entry)) + then Right $ doInsert + else Left $ printf "Entry with name \"%s\" already exists!" name + False -> Right $ doInsert where - name = entryName entry - -insertEntry :: Entry -> Config -> Config -insertEntry entry = - mInsert "user" user - . mInsert "password" password - . mInsert "url" url + name = entryName entry + ni = buildEntrySuffix db name + doInsert = db {config = insertEntry entry ni $ config db} + +insertEntry :: Entry -> String-> Config -> Config +insertEntry entry ni = + mInsert ("user" ++ ni) user + . mInsert ("password" ++ ni) password + . mInsert ("url" ++ ni) url where insert = Config.insert $ entryName entry mInsert k = maybe id (insert k) diff --git a/src/Options.hs b/src/Options.hs index cf010e6..cc70e80 100644 --- a/src/Options.hs +++ b/src/Options.hs @@ -18,6 +18,7 @@ data Options = Options { , passwordOnly :: Bool , gpgOptions :: [String] , dontOpen :: Bool + , forceAdd :: Bool } deriving Show defaultOptions :: Options @@ -29,6 +30,7 @@ defaultOptions = Options { , passwordOnly = False , gpgOptions = [] , dontOpen = False + , forceAdd = False } options :: [OptDescr (Options -> Options)] @@ -43,12 +45,13 @@ options = [ , Option [] ["unlock"] (NoArg (\ opts -> opts { mode = ReleaseLock})) "release write lock for database" , Option [] ["dbfile"] (ReqArg (\s opts -> opts { databaseFile = s }) "FILE") "file where passwords are stored;\ndefaults to ~/.pwsafe/db" - , Option [] ["user"] (ReqArg (\s opts -> opts { userName = Just s }) "USER") "specify a username to be used for a new entry;\nthis option is to be used with --add" + , Option [] ["user"] (ReqArg (\s opts -> opts { userName = Just s }) "USER") "specify a username to be used for an entry;\nthis option is to be used with --add or -q" , Option ['n'] [] (ReqArg (\s opts -> opts { repeatCount = (Just . read) s }) "NUMBER") "copy password n times to clipboard;\ndefaults to 1" , Option [] ["password-only"] (NoArg (\ opts -> opts { passwordOnly = True})) "only copy password to clipboard" , Option ['g'] [] (ReqArg (\s opts -> opts { gpgOptions = (gpgOptions opts) ++ [s]}) "GPG Option") "add a GPG option (repeat to add multiple)" , Option ['d'] [] (NoArg (\ opts -> opts { dontOpen = True })) "don't open a browser when using -q" + , Option ['A'] [] (NoArg (\ opts -> opts { forceAdd = True })) "force add a new account" ] defaultDatabaseFile :: IO String diff --git a/src/Run.hs b/src/Run.hs index 6c02b5f..0552552 100644 --- a/src/Run.hs +++ b/src/Run.hs @@ -18,8 +18,8 @@ run conf cipher h args = do runAction = Action.runAction (Action.mkEnv conf c h) case Options.mode opts of Help -> Options.printHelp - Add url -> withLock $ runAction $ Action.add url (Options.userName opts) - Query s -> runAction $ Action.query s (maybe 1 id $ Options.repeatCount opts) (Options.passwordOnly opts) (Options.dontOpen opts) + Add url -> withLock $ runAction $ Action.add url (Options.userName opts) (Options.forceAdd opts) + Query s -> runAction $ Action.query s (Options.userName opts) (maybe 1 id $ Options.repeatCount opts) (Options.passwordOnly opts) (Options.dontOpen opts) List p -> runAction $ Action.list p Edit -> withLock $ Action.edit c Dump -> runAction $ Action.dump From 5ec6c6cf1d2bae2a86883e548f09cdadfe80c456 Mon Sep 17 00:00:00 2001 From: greg Date: Fri, 4 Oct 2013 15:48:36 +0200 Subject: [PATCH 08/15] Make accounts without usernames (or other fields) work again --- src/Database.hs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Database.hs b/src/Database.hs index 2f6e85f..7adfd6f 100644 --- a/src/Database.hs +++ b/src/Database.hs @@ -2,7 +2,7 @@ module Database (Database, empty, parse, render, addEntry, Entry(..), lookupEntr import Prelude hiding (lookup) -import Data.List (intercalate, sort) +import Data.List (intercalate, sort, nub) import Data.String.Utils import Data.Maybe import Control.DeepSeq @@ -48,9 +48,12 @@ getEntries db name = knownKeys = sort $ Config.keys name (config db) isKey k x = (x == k || startswith (k ++ "_") x) userSuffixes = map (drop 4) $ filter (isKey "user") knownKeys - users = map (lookup . ("user"++)) userSuffixes - passwords = map (lookup . ("password"++)) userSuffixes - urls = map (lookup . ("url"++)) userSuffixes + passSuffixes = map (drop 8) $ filter (isKey "password") knownKeys + urlSuffixes = map (drop 3) $ filter (isKey "url") knownKeys + suffixes = nub $ sort $ userSuffixes ++ passSuffixes ++ urlSuffixes + users = map (lookup . ("user"++)) suffixes + passwords = map (lookup . ("password"++)) suffixes + urls = map (lookup . ("url"++)) suffixes lookup k = Config.lookup name k (config db) makeEntry (us, pw, ur) = Entry { From e0c4a02829576f5533ab5db9ab8b02aa8c1b5036 Mon Sep 17 00:00:00 2001 From: greg Date: Sun, 6 Oct 2013 14:34:13 +0200 Subject: [PATCH 09/15] Rewrote locking so that it works on FreeBSD and the tests pass --- src/Lock.hs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Lock.hs b/src/Lock.hs index 1747e97..1f28370 100644 --- a/src/Lock.hs +++ b/src/Lock.hs @@ -2,19 +2,24 @@ module Lock (acquire, release) where import System.Posix.Files (ownerReadMode, ownerWriteMode, unionFileModes) -import System.Posix.Semaphore (semOpen, OpenSemFlags (..), semUnlink) -import qualified Control.Exception as E +import System.Posix.Semaphore (semOpen, OpenSemFlags (..), + semPost, semTryWait) -- | Try to acquire the lock, return `True` on success acquire :: IO Bool -acquire = (semOpen "/pwsafe" (OpenSemFlags True True) readWriteMode 0 >> return True) `E.catch` handlerFalse +acquire = do + s <- semOpen "/pwsafe" (OpenSemFlags True False) readWriteMode 1 + semTryWait s where readWriteMode = ownerReadMode `unionFileModes` ownerWriteMode -- | Release the lock, return `True` on success release :: IO Bool -release = (semUnlink "/pwsafe" >> return True) `E.catch` handlerFalse +release = do + s <- semOpen "/pwsafe" (OpenSemFlags True False) readWriteMode 1 + free <- semTryWait s + semPost s + return $ not free + where + readWriteMode = ownerReadMode `unionFileModes` ownerWriteMode --- | Return `False` on `E.SomeException` -handlerFalse :: E.SomeException -> IO Bool -handlerFalse = const $ return False From c90a17b2804510cb10bd8a5ad321902c57cb27a1 Mon Sep 17 00:00:00 2001 From: greg Date: Sun, 6 Oct 2013 14:38:50 +0200 Subject: [PATCH 10/15] Fixed a compiler warning --- src/Database.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database.hs b/src/Database.hs index 7adfd6f..10f4300 100644 --- a/src/Database.hs +++ b/src/Database.hs @@ -73,7 +73,7 @@ buildEntrySuffix db name = passSuffixes = map (drop 8) $ filter (isKey "password") knownKeys urlSuffixes = map (drop 3) $ filter (isKey "url") knownKeys suffixes = userSuffixes ++ passSuffixes ++ urlSuffixes - newSuffixes = dropWhile (`elem` suffixes) ("":map (("_" ++) . show) [1..]) + newSuffixes = dropWhile (`elem` suffixes) ("":map (("_" ++) . show) ([1..] :: [Int])) findUser :: [Entry] -> String -> Maybe Entry findUser [] _ = Nothing From 2148935a718f802a4ac9f242f8f7ea89d44b9a78 Mon Sep 17 00:00:00 2001 From: greg Date: Sun, 6 Oct 2013 14:40:09 +0200 Subject: [PATCH 11/15] Make tests pass for the "add gpg parameters" feature --- test/ActionSpec.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ActionSpec.hs b/test/ActionSpec.hs index cb3e3cd..7f40e3f 100644 --- a/test/ActionSpec.hs +++ b/test/ActionSpec.hs @@ -40,7 +40,7 @@ pwsafe args db = do encrypt c db k <- K.newKnob "" K.withFileHandle k "knob.txt" WriteMode $ \h -> do - Run.run (conf clipboardSink) (const c) h (words args) + Run.run (conf clipboardSink) (const $ const c) h (words args) db_ <- Cipher.decrypt c out <- B.unpack `fmap` K.getContents k clipboard <- clipboardAccessor From d4d8c9bf78b7032992bb333cbbfbad1f2b24ab70 Mon Sep 17 00:00:00 2001 From: greg Date: Sun, 6 Oct 2013 14:45:45 +0200 Subject: [PATCH 12/15] Fixed the tests for the "multiple accounts per service" feature --- test/DatabaseSpec.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/DatabaseSpec.hs b/test/DatabaseSpec.hs index 6ac11ef..f6a6b00 100644 --- a/test/DatabaseSpec.hs +++ b/test/DatabaseSpec.hs @@ -38,7 +38,7 @@ instance Arbitrary DatabaseFile where shrink (DatabaseFile _ xs) = [ dbFromList (xs \\ [x]) | x <- xs ] addEntry :: Entry -> Database -> Database -addEntry e db = either error id $ Database.addEntry db e +addEntry e db = either error id $ Database.addEntry db e False entry :: String -> String -> String -> String -> Entry entry name user password url = Database.Entry name (Just user) (Just password) (Just url) @@ -113,10 +113,10 @@ spec = do "user=foo" "password=bar" "url=http://example.com" - lookupEntry db "example.com" `shouldBe` Right (entry "example.com" "foo" "bar" "http://example.com") + lookupEntryUser db "example.com" Nothing `shouldBe` Right ([entry "example.com" "foo" "bar" "http://example.com"]) - it "works on a database with arbitrary entries" $ + it "works on a database with arbitrary unique entries" $ property $ \(DatabaseFile input xs) -> (not . null) xs ==> do x <- elements xs - return $ lookupEntry (parse input) (entryName x) == Right x + return $ lookupEntryUser (parse input) (entryName x) Nothing == Right [x] From 22d8f6b68e868f037d24892eb2eb6731d9021e11 Mon Sep 17 00:00:00 2001 From: greg Date: Sun, 6 Oct 2013 15:34:54 +0200 Subject: [PATCH 13/15] More friendly name for the -g option --- README.md | 45 ++++++++++++++++++++-------------------- bash_completion.d/pwsafe | 2 +- src/Options.hs | 3 ++- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 1c1ae1f..969d908 100644 --- a/README.md +++ b/README.md @@ -139,28 +139,29 @@ Command line options Usage: pwsafe [OPTION]... - --help display this help and exit - -a URL --add=URL add a new entry to the database; the password is - always automatically generated; the username is - generated unless --user is specified - -q TERM --query=TERM lookup a password, the term must match exactly one - entry - -l[TERM] --list[=TERM] list all entries matching the given term - -e --edit invoke vim to edit the database using sensible - defaults (no backup, no swapfile etc) - --dump dump database to stdout - --lock acquire write lock for database - --unlock release write lock for database - --dbfile=FILE file where passwords are stored; - defaults to ~/.pwsafe/db - --user=USER specify a username to be used for an entry; - this option is to be used with --add or -q - -n NUMBER copy password n times to clipboard; - defaults to 1 - --password-only only copy password to clipboard - -g GPG Option add a GPG option (repeat to add multiple) - -d don't open a browser when using -q - -A force add a new account + --help display this help and exit + -a URL --add=URL add a new entry to the database; the password + is always automatically generated; the + username is generated unless --user is + specified + -q TERM --query=TERM lookup a password, the term must match + exactly one entry + -l[TERM] --list[=TERM] list all entries matching the given term + -e --edit invoke vim to edit the database using sensible + defaults (no backup, no swapfile etc) + --dump dump database to stdout + --lock acquire write lock for database + --unlock release write lock for database + --dbfile=FILE file where passwords are stored; + defaults to ~/.pwsafe/db + --user=USER specify a username to be used for an entry; + this option is to be used with --add or -q + -n NUMBER copy password n times to clipboard; + defaults to 1 + --password-only only copy password to clipboard + -g gpgopt --gpg-option=gpgopt add a GPG option (repeat to add multiple) + -d don't open a browser when using -q + -A force add a new account Development =========== diff --git a/bash_completion.d/pwsafe b/bash_completion.d/pwsafe index 91abccf..7a9131f 100644 --- a/bash_completion.d/pwsafe +++ b/bash_completion.d/pwsafe @@ -9,7 +9,7 @@ _pwsafe() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - opts='--help -a --add -q --query -l --list -e --edit --dump --lock --unlock --dbfile --user --password-only -g -d' + opts='--help -a --add -q --query -l --list -e --edit --dump --lock --unlock --dbfile --user --password-only -g --gpg-option -d' case "${prev}" in diff --git a/src/Options.hs b/src/Options.hs index cc70e80..faf76d0 100644 --- a/src/Options.hs +++ b/src/Options.hs @@ -49,7 +49,8 @@ options = [ , Option ['n'] [] (ReqArg (\s opts -> opts { repeatCount = (Just . read) s }) "NUMBER") "copy password n times to clipboard;\ndefaults to 1" , Option [] ["password-only"] (NoArg (\ opts -> opts { passwordOnly = True})) "only copy password to clipboard" - , Option ['g'] [] (ReqArg (\s opts -> opts { gpgOptions = (gpgOptions opts) ++ [s]}) "GPG Option") "add a GPG option (repeat to add multiple)" + , Option ['g'] ["--gpg-option"] + (ReqArg (\s opts -> opts { gpgOptions = (gpgOptions opts) ++ [s]}) "GPG Option") "add a GPG option (repeat to add multiple)" , Option ['d'] [] (NoArg (\ opts -> opts { dontOpen = True })) "don't open a browser when using -q" , Option ['A'] [] (NoArg (\ opts -> opts { forceAdd = True })) "force add a new account" ] From 4e23e65d3158f202f17e351c8741372178125812 Mon Sep 17 00:00:00 2001 From: greg Date: Sun, 6 Oct 2013 15:35:21 +0200 Subject: [PATCH 14/15] Added the -A option to the bash completion script --- bash_completion.d/pwsafe | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bash_completion.d/pwsafe b/bash_completion.d/pwsafe index 7a9131f..a102fc7 100644 --- a/bash_completion.d/pwsafe +++ b/bash_completion.d/pwsafe @@ -9,7 +9,7 @@ _pwsafe() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - opts='--help -a --add -q --query -l --list -e --edit --dump --lock --unlock --dbfile --user --password-only -g --gpg-option -d' + opts='--help -a --add -q --query -l --list -e --edit --dump --lock --unlock --dbfile --user --password-only -g --gpg-option -d -A' case "${prev}" in From 07ba2355181e3a067fdb293887a008b10d7c3183 Mon Sep 17 00:00:00 2001 From: greg Date: Sun, 6 Oct 2013 15:47:33 +0200 Subject: [PATCH 15/15] Added tests for the -g option --- test/OptionsSpec.hs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/OptionsSpec.hs b/test/OptionsSpec.hs index dd815cc..bf37568 100644 --- a/test/OptionsSpec.hs +++ b/test/OptionsSpec.hs @@ -32,3 +32,10 @@ spec = do opts <- Options.get ["--add", "foo", "--help", "--query", "baz"] Options.mode opts `shouldBe` Query "baz" + it "recognizes -g" $ do + opts <- Options.get ["-g", "foo"] + Options.gpgOptions opts `shouldBe` ["foo"] + + it "accepts multiple -g options" $ do + opts <- Options.get ["-g", "foo", "-g", "bar"] + Options.gpgOptions opts `shouldBe` ["foo", "bar"]