{-# LANGUAGE CPP #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_GHC -fno-warn-unused-imports#-} module Data.KeyStore.PasswordManager ( PMConfig(..) , PW(..) , PW_(..) , SessionDescriptor(..) , CollectConfig(..) , defaultCollectConfig , Password(..) , PasswordName(..) , PasswordText(..) , SessionName(..) , EnvVar(..) , passwordManager , defaultHashDescription , defaultSampleScript , hashMasterPassword , bindMasterPassword , setup , login , passwordValid , passwordValid' , isStorePresent , amLoggedIn , isBound , import_ , load , loadPlus , psComment , collect , prime , select , deletePassword , deletePasswordPlus , deleteSession , status , prompt , passwords , passwordsPlus , sessions , infoPassword , infoPassword_ , infoPasswordPlus , infoPasswordPlus_ , dump , collectShell -- password manager CLI internals , passwordManager' , PMCommand(..) , pmCommandParser -- debugging , getStore ) where import Data.KeyStore.Types.PasswordStoreModel import Data.KeyStore.Types import Data.KeyStore.KS.Crypto import Data.KeyStore.KS.CPRNG import Data.KeyStore.Version import qualified Data.Aeson as A import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Base64 as B64 import qualified Data.Text as T import qualified Data.Map as Map import Data.Time import Data.Monoid import Data.API.Types import Data.API.JSON import Data.Maybe import qualified Text.PrettyPrint.ANSI.Leijen as P import Text.Printf import qualified Control.Lens as L import Control.Applicative import Control.Exception import Control.Monad import System.Directory import qualified System.Environment as E import System.SetEnv import System.Exit import System.IO import qualified Options.Applicative as O import Options.Applicative #if MIN_VERSION_time(1,5,0) #else import System.Locale (defaultTimeLocale) #endif -- | The password manager is used for storing locally the passwords and session -- tokens of a single user. The password used to encode the store is stored in -- an environment variable and the passwords and tokens are stored in a file. -- The file and and environment cariable are specified in the 'PWConfig' record. -- (The attributes of each password and session list, including the environment -- variables that they are communicated through, is statically specified -- with the PW class below.) data PMConfig p = PMConfig { _pmc_location :: FilePath -- ^ file in which to store the encrypted passords , _pmc_env_var :: EnvVar -- ^ the environmant variable containing the master password used to secure the store , _pmc_keystore_msg :: String -- ^ error message to be used on failure to locate the keystore , _pmc_password_msg :: String -- ^ error message to be used on failure to locate the master password , _pmc_shell :: IO () -- ^ for firing up an interactive shell on successful login , _pmc_hash_descr :: HashDescription -- ^ for generating has descriptions (can use 'defaultHashDescription' here) , _pmc_allow_dumps :: Bool -- ^ must be true to enable 'dump' commands , _pmc_dump_prefix :: String -- ^ the prefix string to be used in making up the commands from dump scripts , _pmc_sample_script :: Maybe String -- ^ the sample script , _pmc_plus_env_var :: PasswordName -> Maybe EnvVar -- ^ map the dynamic (plus) passwords to their environment variables } -- | The PW class provides all of the information on the bounded enumeration type used to identify the passwords class (Bounded p,Enum p,Eq p, Ord p,Show p) => PW p where -- | the name by which the password is known pwName :: p -> PasswordName pwName = PasswordName . T.pack . show -- | parse a PasswordName into a p parsePwName :: PasswordName -> Maybe p parsePwName = \pnm -> listToMaybe [ p | p<-[minBound..maxBound], pwName p == pnm ] -- | whether the passwords is a session and if so a function for extracting the session name from the secret password text isSession :: p -> Maybe (PasswordText -> Either String SessionDescriptor) isSession = const Nothing -- | whether the password is a one-shot password, needing to be primed to be used isOneShot :: p -> Bool isOneShot = const False -- | the environment variable where the password is expected to be found by the client/deployment scripts enVar :: p -> EnvVar enVar = EnvVar . (T.append "KEY_pw_") . _PasswordName . pwName -- | a brief description of the password in a few words summarize :: p -> String summarize _ = "" -- | a description of the password describe :: p -> String describe p = (T.unpack $ _PasswordName $ pwName p) ++ ": description to follow" -- | we resort to phantom types when we have no other way of passing PW into a -- function (see 'defaultSampleScript') data PW_ p = PW_ cast_pmc :: PMConfig p -> p -> p cast_pmc _ p = p cast_pw :: PW_ p -> p -> p cast_pw _ p = p -- each session is named and may be a one-shot session data SessionDescriptor = SessionDescriptor { _sd_name :: SessionName , _sd_isOneShot :: Bool } deriving (Show) -- | the client calls 'collect' to bind the passwords into the environment data CollectConfig p = CollectConfig { _cc_optional :: Bool -- ^ if True , collect will not report an error if the master password is missing , _cc_active :: [p] -- ^ the list of active passwords for this collection } -- | raise an error if not logged in and collect all of the passwords defaultCollectConfig :: PW p => CollectConfig p defaultCollectConfig = CollectConfig { _cc_optional = True , _cc_active = [minBound..maxBound] } -- | the password manager CLI: it just needs the config and command line passwordManager :: PW p => PMConfig p -> [String] -> IO () passwordManager pmc args = parsePMCommand pmc args >>= passwordManager' pmc -- | a sample 'HashDescription' generator to help with setting up 'PMConfig' defaultHashDescription :: Salt -> HashDescription defaultHashDescription st = HashDescription { _hashd_comment = "PM master password" , _hashd_prf = PRF_sha512 , _hashd_iterations = 5000 , _hashd_width_octets = 32 , _hashd_salt_octets = Octets $ B.length $ _Binary $ _Salt st , _hashd_salt = st } -- | sample sample-script generator to help with setting up 'PMConfig' defaultSampleScript :: PW p => PW_ p -> String -> String defaultSampleScript pw_ pfx = format_dump pfx cmt (map f [minBound..maxBound]) [] where f p = (,) p $ PasswordText $ "secret-" `T.append` _PasswordName (pwName $ cast_pw pw_ p) cmt = PasswordStoreComment $ T.pack "loaded by the sample script" -- | hashing the master password to create the private key for securing the store hashMasterPassword :: PW p => PMConfig p -> String -> PasswordText hashMasterPassword PMConfig{..} pw = PasswordText $ T.pack $ B.unpack $ B64.encode $ _Binary $ _HashData $ _hash_hash $ hashKS_ _pmc_hash_descr $ ClearText $ Binary $ B.pack pw -- | bind the master password in the environment bindMasterPassword :: PW p => PMConfig p -> PasswordText -> IO () bindMasterPassword PMConfig{..} = set_env _pmc_env_var -- | create an empty passowrd store; if the boolean flag is False then -- an interactive shell is fired up with access to the new store; -- if no password is specified then one is read from stdin setup :: PW p => PMConfig p -> Bool -- ^ => don't fire up an interactive shell with access to the new store -> Maybe PasswordText -- ^ the master password -> IO () setup pmc no_li mb_pwt = do -- check there isn't a store there already ex <- doesFileExist _pmc_location when ex $ error $ "password store already exists in: " ++ _pmc_location -- get a password from stdin if we have not been passed one pwt <- maybe (get_pw True pmc) return mb_pwt pwt' <- maybe (get_pw True pmc) return mb_pwt when (pwt/=pwt') $ error "passwords do not match" -- need creation time and comment now <- getCurrentTime let ps = PasswordStore { _ps_comment = PasswordStoreComment $ T.pack $ "Created at " ++ show now , _ps_map = Map.empty , _ps_setup = now } -- write out the new store save_ps pmc (mk_aek pwt) ps when (not no_li) $ login pmc False $ Just pwt where PMConfig{..} = pmc -- | launch an interactive shell with access to the password store; if the bool -- boolean flag is True then it will loop asking for the passwoord until the -- correct password is typed (or an error ocurrs, possibly from a SIGint); -- if no 'PasswordText' is specified then one will be read from stdin login :: PW p => PMConfig p -> Bool -> Maybe PasswordText -> IO () login pmc y mb = do pwt <- maybe (get_pw True pmc) return mb ok <- passwordValid pmc pwt case ok of True -> bindMasterPassword pmc pwt >> good >> _pmc_shell pmc False -> bad >> login pmc y Nothing where good = putStr "*** Login Successful ***\n" bad = bad_f "*** Password Invalid ***\n" bad_f = if y then putStr else error -- | is this the correct master password? passwordValid :: PW p => PMConfig p -> PasswordText -> IO Bool passwordValid pmc pwt = isJust <$> passwordValid' pmc (_pmc_location pmc) pwt -- | is this the correct master password for this keystore? Return the decrypted -- keystore if so. passwordValid' :: PW p => PMConfig p -> FilePath -> PasswordText -> IO (Maybe PasswordStore) passwordValid' pmc fp = password_valid pmc fp . mk_aek -- | is the password store there? isStorePresent :: PW p => PMConfig p -> IO Bool isStorePresent PMConfig{..} = doesFileExist _pmc_location -- | are we currently logged in? amLoggedIn :: PW p => PMConfig p -> IO Bool amLoggedIn pmc = flip catch hdl $ isJust <$> (get_key pmc >>= password_valid pmc (_pmc_location pmc)) where hdl (_::SomeException) = return False -- | is the password/session bound to a value in the store? isBound :: PW p => PMConfig p -> p -> Maybe SessionName -> IO Bool isBound pmc p mb = enquire pmc $ \ps -> return $ case Map.lookup (pwName p) $ _ps_map ps of Nothing -> False Just Password{..} -> maybe True (\snm->Map.member snm $ _pw_sessions) mb -- | import the contents of another keystore into the current keystore import_ :: PW p => PMConfig p -> FilePath -> Maybe PasswordText -> IO () import_ = import__ False -- | import the contents of another keystore into the current keystore import__ :: PW p => Bool -> PMConfig p -> FilePath -> Maybe PasswordText -> IO () import__ x_pps pmc fp0 mb = wrap pmc $ \ps -> do fp <- tilde fp0 ok <- doesFileExist fp when (not ok) $ error "*** password store not found ***" pwt <- maybe (get_pw True pmc) return mb mb_ps <- passwordValid' pmc fp pwt case mb_ps of Nothing -> error "*** Password Invalid ***\n" Just ps' -> return $ Just $ merge_ps x_pps ps ps' where tilde ('~':t@('/':_)) = do mb_hm <- E.lookupEnv "HOME" return $ (fromMaybe "/" mb_hm) ++ t tilde fp = return fp -- | loads a password into the store; if this is a session password and the -- boolean ss is True then the session will be reset to this password also; -- if no 'PasswordText' is specified then one will be read from stdin load :: PW p => PMConfig p -> p -> Maybe PasswordText -> IO () load pmc p mb = wrap pmc $ \ps -> do pwt <- maybe (get_pw False pmc) return mb now <- getCurrentTime case isSession p of Nothing -> load_pwd ps Password { _pw_name = pnm , _pw_text = pwt , _pw_sessions = Map.empty , _pw_isOneShot = isOneShot p , _pw_primed = False , _pw_setup = now } Just ext -> case ext pwt of Left err -> ssn_error $ "failed to load session: " ++ err Right sd -> load_ssn now ps pwt sd where load_ssn now ps pwt SessionDescriptor{..} = load_pwd ps $ L.set pw_text pwt $ L.over pw_sessions (Map.insert _sd_name ssn) $ L.set pw_isOneShot ios $ pw where pw = maybe pw0 id $ Map.lookup pnm $ _ps_map ps pw0 = Password { _pw_name = pnm , _pw_text = pwt , _pw_sessions = Map.empty , _pw_isOneShot = ios , _pw_primed = False , _pw_setup = now } ssn = Session { _ssn_name = _sd_name , _ssn_password = pwt , _ssn_isOneShot = ios , _ssn_setup = UTC now } ios = _sd_isOneShot load_pwd ps pw = return $ Just $ L.over ps_map (Map.insert pnm pw) ps pnm = pwName p -- | load a dynamic password into the Password store loadPlus :: PW p => PMConfig p -> PasswordName -> Maybe PasswordText -> IO () loadPlus pmc pnm_ mb = wrap pmc $ \ps -> do pwt <- maybe (get_pw False pmc) return mb now <- getCurrentTime load_pwd ps Password { _pw_name = pnm , _pw_text = pwt , _pw_sessions = Map.empty , _pw_isOneShot = False , _pw_primed = False , _pw_setup = now } where pnm = PasswordName $ (T.cons '+') $ _PasswordName pnm_ load_pwd ps pw = return $ Just $ L.over ps_map (Map.insert pnm pw) ps -- | set the comment for the password store psComment :: PW p => PMConfig p -> PasswordStoreComment -> IO () psComment pmc cmt = wrap pmc $ \ps -> return $ Just $ L.set ps_comment cmt ps -- | collect the available passwords listed in 'CollectConfig' from the store -- and bind them in their designated environmants variables collect :: PW p => PMConfig p -> CollectConfig p -> IO () collect pmc CollectConfig{..} = wrap_ pmc $ \ps -> do -- set up the environment -- first the static passwords... mapM_ (clct pmc ps) _cc_active -- ... then the dynamic (+) passwords sequence_ [ set_env ev $ _pw_text pw | (pnm_,pw) <- Map.toList $ _ps_map ps , Just pnm <- [is_plus pnm_] , Just ev <- [_pmc_plus_env_var pmc pnm] ] -- now clear down all of the primed passwords return $ Just $ L.over ps_map (Map.map (L.set pw_primed False)) ps where clct :: PW p => PMConfig p -> PasswordStore -> p -> IO () clct _ ps p = case Map.lookup (pwName p) $ _ps_map ps of Just pw | is_primed pw -> set_env (enVar p) (_pw_text pw) _ -> return () wrap_ = if _cc_optional then wrap_def else wrap -- | prime a one-shot password so that it will be availabe on the next collection (probably for a deployment); -- if no password is specified then they are all primed prime :: PW p => PMConfig p -> Bool -> Maybe p -> IO () prime pmc u Nothing = wrap pmc $ \ps -> return $ Just $ L.over ps_map (Map.map (L.set pw_primed $ not u) ) ps prime pmc u (Just p) = wrap pmc $ \ps -> return $ Just $ L.over ps_map (Map.adjust (L.set pw_primed $ not u) (pwName p)) ps -- | select a different session for use select :: PW p => PMConfig p -> Maybe p -> SessionName -> IO () select pmc mb snm = wrap pmc $ \ps -> f ps <$> lookup_session mb snm ps where f ps (p,pw,ssn) = Just $ L.over ps_map (Map.insert (pwName p) (upd pw ssn)) ps upd pw Session{..} = L.set pw_text _ssn_password $ L.set pw_isOneShot _ssn_isOneShot $ L.set pw_primed False $ pw -- | delete a password from the store deletePassword :: PW p => PMConfig p -> p -> IO () deletePassword pmc p = wrap pmc $ \ps -> return $ Just $ L.over ps_map (Map.delete (pwName p)) ps -- | delete a password from the store deletePasswordPlus :: PW p => PMConfig p -> Maybe PasswordName -> IO () deletePasswordPlus pmc Nothing = wrap pmc $ \ps -> return $ Just $ L.over ps_map (Map.filter is_static_pw) ps deletePasswordPlus pmc (Just pnm) = wrap pmc $ \ps -> return $ Just $ L.over ps_map (Map.delete (plussify pnm)) ps -- | delete a session from the store deleteSession :: PW p => PMConfig p -> Maybe p -> SessionName -> IO () deleteSession pmc mb snm = wrap pmc $ \ps -> do trp <- lookup_session mb snm ps chk trp return $ f ps trp where chk (p,pw,ssn) | Just ext <- isSession p , Right sd <- ext $ _pw_text pw , _sd_name sd /= _ssn_name ssn = return () | otherwise = error "cannot delete this session (is it selected?)" f ps (p,pw,_) = Just $ L.over ps_map (Map.insert (pwName p) (L.over pw_sessions (Map.delete snm) pw)) ps -- | print a status line; if @q@ is @True@ then don't output anything and exit -- with fail code 1 if not logged in status :: PW p => PMConfig p -> Bool -> IO () status pmc q = (if q then flip catch hdl else id) $ enquire pmc line where line ps = putStrLn $ "Logged in [" ++ unwords sns' ++ "/" ++ unwords pps' ++ "] (" ++ (T.unpack $ _PasswordStoreComment $ _ps_comment ps) ++ ")" where sns' = sns ++ ["+" ++ show (len pmc (lookup_sessions Nothing (const True) ps) - length sns)] pps' = pps ++ ["+" ++ show (Map.size (_ps_map ps) - length pps)] sns = [ T.unpack $ _SessionName $ _sd_name sd | pw <- Map.elems $ _ps_map ps , let Password{..} = pw , Just p <- [parsePwName _pw_name] , Just prs <- [isSession $ cast_pmc pmc p] , Right sd <- [prs _pw_text] , is_primed pw ] pps = [ T.unpack $ _PasswordName $ _pw_name pw | pw <- Map.elems $ _ps_map ps , _pw_isOneShot pw && is_primed pw ] len :: PMConfig p -> [(p,Password,Session)] -> Int len _ = length hdl (_::SomeException) = exitWith $ ExitFailure 1 -- | print a status apropriate for a prompt prompt :: PW p => PMConfig p -> IO () prompt pmc = flip catch hdl $ do li <- amLoggedIn pmc case li of True -> enquire pmc line False -> putStrLn "*" where line ps = putStrLn $ "[" ++ unwords sns ++ "]" where sns = [ T.unpack $ _SessionName $ _sd_name sd | pw <- Map.elems $ _ps_map ps , let Password{..} = pw , Just p <- [parsePwName _pw_name] , Just prs <- [isSession $ cast_pmc pmc p] , Right sd <- [prs _pw_text] , is_primed pw ] hdl (_::SomeException) = putStrLn "???" -- | list the passwords, one per line; if @a@ is set then all passwords will be listed, -- otherwise just the primed passwords will be listed passwords :: PW p => PMConfig p -> Bool -> IO () passwords pmc br = do tz <- getCurrentTimeZone enquire pmc $ \ps -> putStr $ unlines $ map (fmt tz) $ pws ps where fmt :: PW p => TimeZone -> (p,Password) -> String fmt tz (p,Password{..}) | br = nm_s | otherwise = printf "%-12s %c %2s $%-18s %s %s" nm_s p_c sn_s ev_s su_s cmt where nm_s = T.unpack $ _PasswordName _pw_name p_c = if _pw_isOneShot then prime_char _pw_primed else ' ' sn_s = case Map.size _pw_sessions of 0 -> "" n -> show n ev_s = T.unpack $ _EnvVar $ enVar p su_s = pretty_setup tz _pw_setup cmt = case summarize p of "" -> "" cs -> "# " ++ cs pws ps = [ (cast_pmc pmc p,pwd) | p <- [minBound..maxBound] , Just pwd <- [Map.lookup (pwName p) $ _ps_map ps] ] -- | list all of the dynamic (+) passwords passwordsPlus :: PW p => PMConfig p -> Bool -> IO () passwordsPlus pmc br = do tz <- getCurrentTimeZone enquire pmc $ \ps -> putStr $ unlines $ map (fmt tz) $ pws ps where fmt tz (pnm,Password{..}) | br = nm_s | otherwise = printf "+%-12s $%-18s %s" nm_s ev_s su_s where nm_s = T.unpack $ _PasswordName pnm ev_s = T.unpack $ _EnvVar $ fromMaybe "?" $ _pmc_plus_env_var pmc pnm su_s = pretty_setup tz _pw_setup pws ps = [ (pnm,pw) | (pnm_,pw) <- Map.toList $ _ps_map ps , Just pnm <- [is_plus pnm_] ] -- | list the sessions, one per line; if @p@ is specified then all of the -- sessions are listed for that password sessions :: PW p => PMConfig p -> Bool -- ^ list active sessions only -> Bool -- ^ list only the session identifiers -> Maybe p -- ^ if specified, then only the sessions on this password -> IO () sessions pmc a b mb = do tz <- getCurrentTimeZone enquire pmc $ \ps -> let trps = case a of True -> [ trp | trp@(_,pw,_)<-trps_, active_session trp && is_primed pw] False -> trps_ trps_ = lookup_sessions mb (const True) ps in putStr $ unlines $ map (fmt tz) trps where fmt tz trp@(_,Password{..},Session{..}) = case b of True -> printf "%s" sn_s False -> case sgl of True -> printf "%-16s %c %s %s" sn_s p_c su_s a_s False -> printf "%-12s %-16s %c %s %s" pn_s sn_s p_c su_s a_s where pn_s = T.unpack $ _PasswordName _pw_name sn_s = T.unpack $ _SessionName _ssn_name p_c = if _ssn_isOneShot then prime_char False else ' ' su_s = pretty_setup tz $ _UTC _ssn_setup a_s = if active_session trp then "[ACTIVE]" else "" :: String sgl = length [ () | p<-[minBound..maxBound], isJust $ isSession $ cast_pmc pmc p ] == 1 -- | print the info, including the text descriton, for an individual passowrd infoPassword :: PW p => PMConfig p -> Bool -- ^ True => show the password secret text -> p -- ^ the password to show -> IO () infoPassword pmc sh_s p = do doc <- infoPassword_ pmc sh_s p putStr $ P.displayS (P.renderPretty 0.75 120 doc) "" -- | get the info on a password infoPassword_ :: PW p => PMConfig p -> Bool -> p -> IO P.Doc infoPassword_ pmc sh_s p = do tz <- getCurrentTimeZone enquire pmc $ \ps -> return $ maybe P.empty (mk tz) $ Map.lookup pnm $ _ps_map ps where mk tz pw@Password{..} = heading P.<$$> P.indent 4 ( sssions P.<> primed P.<$$> evar P.<$$> secret P.<> loaded P.<$$> P.empty P.<$$> descr ) P.<$$> P.empty where heading = P.bold $ P.string $ T.unpack $ _PasswordName pnm sssions = case isSession p of Nothing -> P.empty Just xt -> (line "sessions" $ fmt_sns xt) P.<$$> P.empty primed = line "primed" $ if is_primed pw then "yes" else "no" evar = line "env var" $ T.unpack $ _EnvVar $ enVar p loaded = line "loaded" $ pretty_setup tz $ _pw_setup descr = P.string $ describe p secret = case sh_s of True -> (line "secret" $ T.unpack $ _PasswordText _pw_text) P.<$$> P.empty False -> P.empty fmt_sns xt = sn ++ " / " ++ unwords (map (T.unpack . _SessionName) $ filter fl sns) where sn = either (\s->"<<"++s++">>") (T.unpack . _SessionName . _sd_name ) ei fl = either (\_ _->False) (\sd sn'->_sd_name sd/=sn') ei ei = xt _pw_text sns = Map.keys _pw_sessions line :: String -> String -> P.Doc line nm vl = P.bold(P.string $ ljust 8 nm) P.<> P.string " : " P.<> P.hang 8 (P.string vl) pnm = pwName p ljust n s = s ++ replicate (max 0 (n-length s)) ' ' -- | print the info for a dynamic (+) password infoPasswordPlus :: PW p => PMConfig p -> Bool -> PasswordName -> IO () infoPasswordPlus pmc sh_s pnm = do doc <- infoPasswordPlus_ pmc sh_s pnm putStr $ P.displayS (P.renderPretty 0.75 120 doc) "" -- | get the info on a dynamic (+) password infoPasswordPlus_ :: PW p => PMConfig p -> Bool -> PasswordName -> IO P.Doc infoPasswordPlus_ pmc sh_s pnm = do tz <- getCurrentTimeZone enquire pmc $ \ps -> return $ maybe P.empty (mk tz) $ Map.lookup (plussify pnm) $ _ps_map ps where mk tz Password{..} = heading P.<$$> P.indent 4 ( evar P.<> secret P.<> loaded ) P.<$$> P.empty where heading = P.bold $ P.string $ "+" ++ T.unpack (_PasswordName pnm) evar = case _pmc_plus_env_var pmc pnm of Nothing -> P.empty Just ev -> (line "env var" $ T.unpack $ _EnvVar $ ev ) P.<$$> P.empty loaded = line "loaded" $ pretty_setup tz $ _pw_setup secret = case sh_s of True -> (line "secret" $ T.unpack $ _PasswordText _pw_text) P.<$$> P.empty False -> P.empty line :: String -> String -> P.Doc line nm vl = P.bold(P.string $ ljust 8 nm) P.<> P.string " : " P.<> P.hang 8 (P.string vl) ljust n s = s ++ replicate (max 0 (n-length s)) ' ' -- | dump the store in a s script that can be used to reload it dump :: PW p => PMConfig p -> Bool -> IO () dump pmc inc_ssns = enquire pmc dmp >> prime pmc True Nothing where dmp ps@PasswordStore{..} = putStr $ format_dump (_pmc_dump_prefix pmc) _ps_comment al_l al_s where al_l = [ (p,_pw_text pw) | p <- [minBound..maxBound] , Just pw <- [Map.lookup (pwName $ cast_pmc pmc p) _ps_map] , isNothing $ isSession p , is_primed pw ] ++ [ (p,_ssn_password ssn) | inc_ssns , (p,_,ssn) <- lookup_sessions Nothing (const True) ps ] al_s = [ (p,_sd_name sd) | inc_ssns , p <- [minBound..maxBound] , Just pw <- [Map.lookup (pwName $ cast_pmc pmc p) _ps_map] , Just ext <- [isSession p] , Right sd <- [ext $ _pw_text pw] ] -- | collect the passowrds, bthem into the environmant and launch an interacive shell collectShell :: PW p => PMConfig p -> IO () collectShell pmc = collect pmc defaultCollectConfig >> _pmc_shell pmc -- | check whether a password is primed for use is_primed :: Password -> Bool is_primed Password{..} = not _pw_isOneShot || _pw_primed -- | lookup a session in a password store, possibly specifying the password it belogs to; exactly -- one session must be found, otherwise an error is generated lookup_session :: PW p => Maybe p -> SessionName -> PasswordStore -> IO (p,Password,Session) lookup_session mb snm ps = case lookup_sessions mb (==snm) ps of [] -> err "session not loaded" [r] -> return r _ -> err "matches multiple sessions" where err msg = ssn_error $ "lookup_session: " ++ T.unpack(_SessionName snm) ++ ": " ++ msg -- | lookup all of the sessions in a password store lookup_sessions :: PW p => Maybe p -> (SessionName->Bool) -> PasswordStore -> [(p,Password,Session)] lookup_sessions mb f ps = [ (p,pw,ssn) | p <- [minBound..maxBound] , maybe True (p==) mb , isJust $ isSession p , let pnm = pwName p , Just pw <- [Map.lookup pnm $ _ps_map ps] , ssn <- filter (f . _ssn_name) $ Map.elems $ _pw_sessions pw ] active_session :: PW p => (p,Password,Session) -> Bool active_session (p,Password{..},Session{..}) = not $ null [ () | Just ext <- [isSession p] , Right sd <- [ext _pw_text] , _sd_name sd == _ssn_name ] -- | read a passord from stdin and hash it get_pw :: PW p => Bool -> PMConfig p -> IO PasswordText get_pw hp pmc = do hSetEcho stdin False putStr "Password: " hFlush stdout pw <- getLine putChar '\n' hSetEcho stdin True return $ cond_hash hp pmc pw cond_hash :: PW p => Bool -> PMConfig p -> String -> PasswordText cond_hash False _ = PasswordText . T.pack cond_hash True pmc = hashMasterPassword pmc -- | use a '+' to represent a primed one-shot password,'-' otherwise prime_char :: Bool -> Char prime_char is_p = if is_p then '+' else '-' -- | make up a script for loading a password store format_dump :: PW p => String -- ^ the prefix for each script command line -> PasswordStoreComment -- ^ the store comment -> [(p,PasswordText)] -- ^ the passwords to load -> [(p,SessionName)] -- ^ the sessions to select -> String format_dump pfx ps_cmt al_l al_s = unlines $ (printf "%s comment %s ;" pfx $ esc $ T.unpack $ _PasswordStoreComment ps_cmt) : [ printf "%s load %-12s %-20s %-30s ;" pfx pnm_s ptx_s $ cmt_s p | (p,ptx) <- al_l , let pnm_s = T.unpack $ _PasswordName $ pwName p , let ptx_s = T.unpack $ _PasswordText ptx ] ++ [ printf "%s select -p %s %s ;" pfx pnm_s snm_s | (p,snm) <- al_s , let pnm_s = T.unpack $ _PasswordName $ pwName p , let snm_s = T.unpack $ _SessionName snm ] where cmt_s p = case summarize p of "" -> "" s -> "# " ++ esc s esc s = '\'' : foldr tr "\'" s where tr '\'' t = '\\' : '\'' : t tr c t = c : t wrap_def :: PW p => PMConfig p -> (PasswordStore -> IO (Maybe PasswordStore)) -> IO () wrap_def pmc f = maybe (return ()) (wrap' pmc f) =<< get_key' pmc wrap :: PW p => PMConfig p -> (PasswordStore -> IO (Maybe PasswordStore)) -> IO () wrap pmc f = get_key pmc >>= wrap' pmc f wrap' :: PW p => PMConfig p -> (PasswordStore -> IO (Maybe PasswordStore)) -> AESKey -> IO () wrap' pmc f aek = do pws <- load_ps pmc aek mb <- f pws maybe (return ()) (save_ps pmc aek) mb getStore :: PW p => PMConfig p -> IO PasswordStore getStore pmc = enquire pmc return enquire :: PW p => PMConfig p -> (PasswordStore -> IO a) -> IO a enquire pmc f = do aek <- get_key pmc load_ps pmc aek >>= f password_valid :: PW p => PMConfig p -> FilePath -> AESKey -> IO (Maybe PasswordStore) password_valid pmc fp aek = catch ld hd where ld = Just <$> load_ps_ pmc fp aek hd (_::SomeException) = return Nothing load_ps :: PW p => PMConfig p -> AESKey -> IO PasswordStore load_ps pmc = load_ps_ pmc (_pmc_location pmc) load_ps_ :: PW p => PMConfig p -> FilePath -> AESKey -> IO PasswordStore load_ps_ pmc fp aek = do aed <- load_ps' pmc fp case decodeWithErrs $ BL.fromChunks [_Binary $ _ClearText $ decryptAES aek aed] of Right pws -> return pws Left ers -> error $ prettyJSONErrorPositions ers save_ps :: PW p => PMConfig p -> AESKey -> PasswordStore -> IO () save_ps pmc aek pws = do iv <- random_bytes sizeAesIV IV save_ps' pmc $ encryptAES aek iv $ ClearText $ Binary $ BL.toStrict $ A.encode pws load_ps' :: PW p => PMConfig p -> FilePath -> IO AESSecretData load_ps' PMConfig{..} fp = flip catch hdl $ do (iv,ct) <- B.splitAt (_Octets sizeAesIV) <$> B.readFile fp return AESSecretData { _asd_iv = IV $ Binary iv , _asd_secret_data = SecretData $ Binary ct } where hdl (_::SomeException) = error _pmc_keystore_msg -- | marge in the second password store into the first, all definitions in -- the second passwords store, except the store's creation time, which is -- taken from the first store; any sessions are also merged with the -- sessions in the second store taking precedence merge_ps :: Bool -> PasswordStore -> PasswordStore -> PasswordStore merge_ps x_pps ps ps0' = PasswordStore { _ps_comment = _ps_comment ps' , _ps_map = Map.unionWith f (_ps_map ps) (_ps_map ps') , _ps_setup = _ps_setup ps } where f pw pw' = L.over pw_sessions (flip Map.union $ _pw_sessions pw) pw' ps' = case x_pps of True -> L.over ps_map (Map.filter is_static_pw) ps0' False -> ps0' is_static_pw :: Password -> Bool is_static_pw Password{..} = case T.unpack $ _PasswordName _pw_name of '+':_ -> False _ -> True random_bytes :: Octets -> (Binary->a) -> IO a random_bytes sz f = f . Binary . fst . generateCPRNG (_Octets sz) <$> newCPRNG save_ps' :: PW p => PMConfig p -> AESSecretData -> IO () save_ps' PMConfig{..} AESSecretData{..} = B.writeFile _pmc_location $ B.concat [iv_bs,ct_bs] where iv_bs = _Binary $ _IV _asd_iv ct_bs = _Binary $ _SecretData _asd_secret_data get_key :: PW p => PMConfig p -> IO AESKey get_key pmc = get_key' pmc >>= maybe (not_logged_in_err pmc) return not_logged_in_err :: PW p => PMConfig p -> IO a not_logged_in_err pmc@PMConfig{..} = do ex <- isStorePresent pmc error $ if ex then _pmc_password_msg else _pmc_keystore_msg get_key' :: PW p => PMConfig p -> IO (Maybe AESKey) get_key' PMConfig{..} = fmap mk_aek' <$> E.lookupEnv var where var = T.unpack $ _EnvVar _pmc_env_var mk_aek :: PasswordText -> AESKey mk_aek = mk_aek' . T.unpack . _PasswordText mk_aek' :: String -> AESKey mk_aek' = AESKey . Binary . either err id . B64.decode . B.pack where err = error "bad format for the master password" pretty_setup :: TimeZone -> UTCTime -> String pretty_setup tz = formatTime defaultTimeLocale "%F %H:%M" . utcToZonedTime tz set_env :: EnvVar -> PasswordText -> IO () set_env (EnvVar ev) (PasswordText pt) = setEnv (T.unpack ev) (T.unpack pt) ssn_error :: String -> a ssn_error msg = error $ "session manager error: " ++ msg -- -- The Command Line Parser -- -- | run a password manager command passwordManager' :: PW p => PMConfig p -> PMCommand p -> IO () passwordManager' pmc pmcd = case pmcd of PMCD_version -> putStrLn version PMCD_setup nl mb_t -> setup pmc nl mb_t PMCD_login y mb_t -> login pmc y mb_t PMCD_import x_pps fp mb_t -> import__ x_pps pmc fp mb_t PMCD_load p mb_t -> load pmc p mb_t PMCD_load_plus pnm mb_t -> loadPlus pmc pnm mb_t PMCD_comment cmt -> psComment pmc cmt PMCD_prime u p -> prime pmc u $ Just p PMCD_prime_all u -> prime pmc u Nothing PMCD_select mb snm -> select pmc mb snm PMCD_delete_password p -> deletePassword pmc p PMCD_delete_password_plus pnm -> deletePasswordPlus pmc pnm PMCD_delete_session mb snm -> deleteSession pmc mb snm PMCD_status q -> status pmc q PMCD_prompt -> prompt pmc PMCD_passwords b -> passwords pmc b PMCD_passwords_plus b -> passwordsPlus pmc b PMCD_session b -> sessions pmc True b Nothing PMCD_sessions b mb -> sessions pmc False b mb PMCD_info s p -> infoPassword pmc s p PMCD_info_plus s pnm -> infoPasswordPlus pmc s pnm PMCD_dump s -> dump pmc s PMCD_collect -> collectShell pmc PMCD_sample_script -> putStr $ maybe "" id $ _pmc_sample_script pmc -- | the abstract syntax for the passowd manager commands data PMCommand p = PMCD_version | PMCD_setup Bool (Maybe PasswordText) | PMCD_login Bool (Maybe PasswordText) | PMCD_import Bool FilePath (Maybe PasswordText) | PMCD_load p (Maybe PasswordText) | PMCD_load_plus PasswordName (Maybe PasswordText) | PMCD_comment PasswordStoreComment | PMCD_prime Bool p | PMCD_prime_all Bool | PMCD_select (Maybe p) SessionName | PMCD_delete_password p | PMCD_delete_password_plus (Maybe PasswordName) | PMCD_delete_session (Maybe p) SessionName | PMCD_status Bool | PMCD_prompt | PMCD_passwords Bool | PMCD_passwords_plus Bool | PMCD_session Bool | PMCD_sessions Bool (Maybe p) | PMCD_info Bool p | PMCD_info_plus Bool PasswordName | PMCD_dump Bool | PMCD_collect | PMCD_sample_script deriving (Show) -- | parse a passwword manager command parsePMCommand :: PW p => PMConfig p -> [String] -> IO (PMCommand p) parsePMCommand pmc = run_parse $ command_info pmc command_info :: PW p => PMConfig p -> ParserInfo (PMCommand p) command_info pmc = O.info (helper <*> pmCommandParser pmc) ( fullDesc <> progDesc "a simple password manager" <> header "pm - sub-command for managing the password store" <> footer "'ks COMMAND --help' to get help on each command") pmCommandParser :: PW p => PMConfig p -> Parser (PMCommand p) pmCommandParser pmc = subparser $ f $ g $ command "version" pi_version <> command "setup" (pi_setup pmc) <> command "login" (pi_login pmc) <> command "import" (pi_import pmc) <> command "load" (pi_load pmc) <> command "comment" pi_comment <> command "prime" pi_prime <> command "prime-all" pi_prime_all <> command "select" pi_select <> command "delete-password" (pi_delete_password pmc) <> command "delete-all-plus-passwords" pi_delete_all_plus_passwords <> command "delete-session" pi_delete_session <> command "status" pi_status <> command "prompt" pi_prompt <> command "passwords" pi_passwords <> command "passwords-plus" pi_passwords_plus <> command "session" pi_session <> command "sessions" pi_sessions <> command "info" (pi_info pmc) <> command "collect" pi_collect where s = command "sample-load-script" pi_sample_script d = command "dump" pi_dump f = case _pmc_sample_script pmc of Nothing -> id Just _ -> (<> s) g = case _pmc_allow_dumps pmc of True -> (<> d) False -> id pi_version :: ParserInfo (PMCommand p) pi_version = h_info (helper <*> pure PMCD_version) (progDesc "report the version of this package") pi_setup :: PW p => PMConfig p -> ParserInfo (PMCommand p) pi_setup pmc = h_info (helper <*> (PMCD_setup <$> p_no_login_sw <*> optional (p_password_text True pmc))) (progDesc "setup the password store") pi_login :: PW p => PMConfig p -> ParserInfo (PMCommand p) pi_login pmc = h_info (helper <*> (PMCD_login <$> p_loop_sw <*> optional (p_password_text True pmc))) (progDesc "login to the password manager") pi_import :: PW p => PMConfig p -> ParserInfo (PMCommand p) pi_import pmc = h_info (helper <*> (PMCD_import <$> p_x_pps <*> p_store_fp <*> optional (p_password_text True pmc))) (progDesc "import the contents of another store") pi_load :: PW p => PMConfig p -> ParserInfo (PMCommand p) pi_load pmc = h_info (helper <*> p_load_command pmc) (progDesc "load a password into the store") pi_comment :: PW p => ParserInfo (PMCommand p) pi_comment = h_info (helper <*> (PMCD_comment <$> p_ps_comment)) (progDesc "load a password into the store") pi_prime :: PW p => ParserInfo (PMCommand p) pi_prime = h_info (helper <*> (PMCD_prime <$> p_unprime_sw <*> p_pw_id)) (progDesc "(un) prime a password for use") pi_prime_all :: ParserInfo (PMCommand p) pi_prime_all = h_info (helper <*> (PMCD_prime_all <$> p_unprime_sw)) (progDesc "(un)prime all of the passwords") pi_select :: PW p => ParserInfo (PMCommand p) pi_select = h_info (helper <*> (PMCD_select <$> optional p_pw_id_opt <*> p_session_name)) (progDesc "select a client session") pi_delete_password :: PW p => PMConfig p -> ParserInfo (PMCommand p) pi_delete_password pmc = h_info (helper <*> p_delete_password pmc) (progDesc "delete a password from the store") pi_delete_all_plus_passwords :: ParserInfo (PMCommand p) pi_delete_all_plus_passwords = h_info (helper <*> pure (PMCD_delete_password_plus Nothing)) (progDesc "delete all dynamic (plus) passwords forom the store") pi_delete_session :: PW p => ParserInfo (PMCommand p) pi_delete_session = h_info (helper <*> (PMCD_delete_session <$> optional p_pw_id_opt <*> p_session_name)) (progDesc "delete a client session") pi_status :: ParserInfo (PMCommand p) pi_status = h_info (helper <*> (PMCD_status <$> p_quiet_sw)) (progDesc "report the status of the password manager") pi_prompt :: ParserInfo (PMCommand p) pi_prompt = h_info (helper <*> (pure PMCD_prompt)) (progDesc $ "report the condensed status of the password manager"++ " (suitable for embedding in a shell prompt") pi_passwords :: ParserInfo (PMCommand p) pi_passwords = h_info (helper <*> (PMCD_passwords <$> p_brief_sw)) (progDesc "list the passwords") pi_passwords_plus :: ParserInfo (PMCommand p) pi_passwords_plus = h_info (helper <*> (PMCD_passwords_plus <$> p_brief_sw)) (progDesc "list the dynamic ('+'') passwords") pi_session :: PW p => ParserInfo (PMCommand p) pi_session = h_info (helper <*> (PMCD_session <$> p_brief_sw)) (progDesc "list the sessions") pi_sessions :: PW p => ParserInfo (PMCommand p) pi_sessions = h_info (helper <*> (PMCD_sessions <$> p_brief_sw <*> optional p_pw_id)) (progDesc "list the sessions") pi_info :: PW p => PMConfig p -> ParserInfo (PMCommand p) pi_info pmc = h_info (helper <*> p_info pmc) (progDesc "print out the info on a password, including desriptive text") pi_dump :: PW p => ParserInfo (PMCommand p) pi_dump = h_info (helper <*> (PMCD_dump <$> p_sessions_sw)) (progDesc "dump the passwords on the output as a load script") pi_collect :: PW p => ParserInfo (PMCommand p) pi_collect = h_info (helper <*> (pure PMCD_collect)) (progDesc "collect the passwords and launch an interacive shell") pi_sample_script :: ParserInfo (PMCommand p) pi_sample_script = h_info (helper <*> (pure PMCD_sample_script)) (progDesc "print a sample script to define keystore passwords in the environment (PM edition)") p_load_command, p_delete_password, p_info :: PW p => PMConfig p -> Parser (PMCommand p) p_load_command pmc = f <$> p_pw pmc <*> optional (p_password_text False pmc) <* optional p_load_comment where f ei op_p = either (flip PMCD_load op_p) (flip PMCD_load_plus op_p) ei p_delete_password pmc = either PMCD_delete_password (PMCD_delete_password_plus . Just) <$> p_pw pmc p_info pmc = f <$> p_secret_sw <*> p_pw pmc where f s_sw (Left p ) = PMCD_info s_sw p f s_sw (Right pnm) = PMCD_info_plus s_sw pnm -- switches p_brief_sw :: Parser Bool p_brief_sw = switch (short 'b' <> long "brief" <> help "list the identifiers only") p_loop_sw :: Parser Bool p_loop_sw = switch (short 'l' <> long "loop" <> help "on failure prompt for a new password and try again") p_no_login_sw :: Parser Bool p_no_login_sw = switch (short 'n' <> long "no-login" <> help "do not launch an interactive shell") p_quiet_sw :: Parser Bool p_quiet_sw = switch (short 'q' <> long "quiet" <> help "don't print anything but report with error codes (0=>logged in)") p_secret_sw :: Parser Bool p_secret_sw = switch (short 's' <> long "secret" <> help "show the secret password") p_sessions_sw :: Parser Bool p_sessions_sw = switch (long "sessions" <> help "include the sessions") p_unprime_sw :: Parser Bool p_unprime_sw = switch (short 'u' <> long "unprime" <> help "clear the prime status") p_x_pps :: Parser Bool p_x_pps = switch (short 'x' <> long "exclude-plus-passwords" <> help "exclude the dynamic (plus) passwords") -- options p_pw_id_opt :: PW p => Parser p p_pw_id_opt = option (eitherReader $ maybe (Left "password-id not recognised") return . parsePwName . PasswordName . T.pack) $ long "id" <> short 'p' <> metavar "PASSWORD-ID" <> help "a password ID" -- arguments p_comment :: Parser String p_comment = unwords <$> many p_word p_hash :: Parser () p_hash = argument (eitherReader $ \s->if s=="#" then return () else Left "# expected") $ metavar "#" h_info :: Parser a -> InfoMod a -> ParserInfo a h_info pr = O.info (helper <*> pr) p_load_comment :: Parser () p_load_comment = const () <$> optional (p_hash <* p_comment) p_password_text :: PW p => Bool -> PMConfig p -> Parser PasswordText p_password_text hp pmc = argument (eitherReader $ Right . cond_hash hp pmc) $ metavar "PASSWORD-TEXT" <> help "the text of the password" p_pw :: PW p => PMConfig p -> Parser (Either p PasswordName) p_pw pmc = argument (eitherReader $ maybe (Left "bad password syntax") Right . prs) $ metavar "PASSWORD" <> help "a static or dynamic (+) password name" where prs s = Left <$> (parsePwName $ PasswordName $ T.pack s) <|> Right <$> (parse_plus_pw pmc s) p_pw_id :: PW p => Parser p p_pw_id = argument (eitherReader $ maybe (Left "bad password syntax") return . parsePwName . PasswordName . T.pack) $ metavar "PASSWORD-ID" <> help "a password ID" p_ps_comment :: Parser PasswordStoreComment p_ps_comment = PasswordStoreComment . T.pack <$> p_comment p_session_name :: Parser SessionName p_session_name = argument (eitherReader $ Right . SessionName . T.pack) $ metavar "SESSION" <> help "a session name" p_store_fp :: Parser FilePath p_store_fp = argument (eitherReader Right) $ metavar "STORE" <> help "file containing the password store to import" p_word :: Parser String p_word = argument (eitherReader Right) $ metavar "WORD" -- run_parse run_parse :: ParserInfo a -> [String] -> IO a run_parse pinfo args = case execParserPure (prefs idm) pinfo args of Success a -> return a Failure failure -> do progn <- E.getProgName let (msg, exit, _) = execFailure failure progn case exit of ExitSuccess -> putStrLn $ show msg _ -> hPutStrLn stderr $ show msg exitWith exit CompletionInvoked compl -> do progn <- E.getProgName msg <- execCompletion compl progn putStr msg exitWith ExitSuccess -- plus helpers parse_plus_pw :: PW p => PMConfig p -> String -> Maybe PasswordName parse_plus_pw pmc s_ = case s_ of '+':s | isJust $ _pmc_plus_env_var pmc pnm -> Just pnm where pnm = PasswordName $ T.pack s _ -> Nothing plussify :: PasswordName -> PasswordName plussify = PasswordName . (T.cons '+') . _PasswordName is_plus :: PasswordName -> Maybe PasswordName is_plus pnm = case T.unpack $ _PasswordName pnm of '+':s -> Just $ PasswordName $ T.pack s _ -> Nothing