Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 44 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -91,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
---------------------------------------------

Expand All @@ -111,37 +124,44 @@ 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
====================

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 a new entry;
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
--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
===========
Expand Down
2 changes: 1 addition & 1 deletion bash_completion.d/pwsafe
Original file line number Diff line number Diff line change
Expand Up @@ -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 --gpg-option -d -A'

case "${prev}" in

Expand Down
1 change: 1 addition & 0 deletions pwsafe.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ executable pwsafe
, deepseq
, unix
, config-ng
, MissingH
main-is:
Main.hs

Expand Down
28 changes: 19 additions & 9 deletions src/Action.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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(..))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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_}
Expand All @@ -88,24 +95,27 @@ 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 -> ActionM ()
query kw n passwordOnly = 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
open url
unless noOpen $ open url
forM_ (entryUser x)
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)

Expand Down
15 changes: 13 additions & 2 deletions src/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
Expand All @@ -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
93 changes: 69 additions & 24 deletions src/Database.hs
Original file line number Diff line number Diff line change
@@ -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, nub)
import Data.String.Utils
import Data.Maybe
import Control.DeepSeq
import Text.Printf (printf)

Expand All @@ -26,40 +28,83 @@ 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
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 {
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..] :: [Int]))

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

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)
Expand Down
19 changes: 12 additions & 7 deletions src/Lock.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion src/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading