├── .gitignore ├── Setup.hs ├── EntryBook.hs ├── Encrypt.hs ├── hpm.cabal ├── BookLibrary.hs ├── LICENSE ├── Types.hs ├── Tools.hs ├── README.md └── Main.hs /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *~ 3 | *.hi 4 | build 5 | clean 6 | hpm 7 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /EntryBook.hs: -------------------------------------------------------------------------------- 1 | module EntryBook ( 2 | 3 | ) where 4 | 5 | -- Reads the entry book and displays the password entries (service & ids) 6 | -------------------------------------------------------------------------------- /Encrypt.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Encrypt ( 4 | hashPassword 5 | , encrypt 6 | , decrypt 7 | ) where 8 | 9 | import Codec.Crypto.SimpleAES (IV, crypt, Direction(..), Mode(..)) 10 | import Data.ByteString (ByteString) 11 | import qualified Data.ByteString.Lazy as B (ByteString, toStrict, filter, notElem) 12 | 13 | 14 | -- Hash a (master) password with SHA512 15 | hashPassword :: B.ByteString -> ByteString 16 | --hashPassword = hash SHA512ibrary 17 | hashPassword = B.toStrict -- enough for now : the library book is encrypted, so master passwords never appear in clear 18 | 19 | -- Key for encrypting with SimpleAES 20 | aesKey :: ByteString 21 | aesKey = "1234567890123456" 22 | 23 | myIV :: IV 24 | myIV = "qwertyuiopasdfgh" 25 | 26 | -- Encrypt an entry book's contents 27 | encrypt :: B.ByteString -> IO (B.ByteString) 28 | --encrypt = encryptMsg CBC aesKey 29 | encrypt msg = return $ crypt CBC aesKey myIV Encrypt msg 30 | 31 | -- Decrypt an entry book's contents 32 | decrypt :: B.ByteString -> B.ByteString 33 | --decrypt = decryptMsg CBC aesKey 34 | decrypt msg = B.filter (`B.notElem` "\NUL") $ crypt CBC aesKey myIV Decrypt msg 35 | -------------------------------------------------------------------------------- /hpm.cabal: -------------------------------------------------------------------------------- 1 | name: hpm 2 | version: 0.1.0.0 3 | synopsis: A simple password manager 4 | description: A simple password manager written in Haskell. 5 | homepage: http://github.com/nschoe/hpm 6 | license: BSD3 7 | license-file: LICENSE 8 | author: Nicolas Schoemaeker 9 | maintainer: ns.schoe@gmail.com 10 | copyright: 2014 Nicolas Schoemaeker 11 | category: System 12 | build-type: Simple 13 | extra-source-files: README.md 14 | cabal-version: >=1.10 15 | 16 | executable hpm 17 | main-is: Main.hs 18 | -- other-modules: 19 | other-extensions: OverloadedStrings 20 | build-depends: base >=4.5 && <4.8, 21 | bytestring >=0.10 && <0.11, 22 | haskeline >=0.7 && <0.8, 23 | vector >=0.10 && <0.11, 24 | directory >=1.2 && <1.3, 25 | process >=1.1 && <1.2, 26 | filepath >=1.3 && <1.4, 27 | cassava >=0.3 && <0.4, 28 | SimpleAES >=0.4 && <0.5 29 | -- hs-source-dirs: 30 | default-language: Haskell2010 31 | ghc-options: -O2 -Wall -------------------------------------------------------------------------------- /BookLibrary.hs: -------------------------------------------------------------------------------- 1 | module BookLibrary ( 2 | askMasterPwd 3 | , askPassword 4 | ) where 5 | 6 | import qualified Data.ByteString.Lazy as B (fromStrict, ByteString) 7 | import qualified Data.ByteString.Lazy.Char8 as LB8 (pack) 8 | import Types (PwdHash) 9 | import Encrypt (hashPassword) 10 | import System.Console.Haskeline (runInputT, getPassword, defaultSettings) 11 | 12 | -- Prompts the user for its master password 13 | askMasterPwd :: IO PwdHash 14 | askMasterPwd = askPassword (Just "Please type your master password : ") >>= return . B.fromStrict . hashPassword 15 | 16 | {- Asks the user to type a password, secure the typing in the terminal. 17 | Optionally passes a prompt message. 18 | -} 19 | 20 | askPassword :: Maybe String -> IO (B.ByteString) 21 | {-askPassword Nothing = askPassword' 22 | askPassword (Just message) = putStr message >> hFlush stdout >> askPassword' 23 | 24 | askPassword' :: IO (B.ByteString) 25 | askPassword' = getLine >>= return . B.fromStrict . B8.pack 26 | -} 27 | askPassword Nothing = askPassword' "" 28 | askPassword (Just msg) = askPassword' msg 29 | 30 | askPassword' :: String -> IO (B.ByteString) 31 | askPassword' msg = do 32 | pwd <- runInputT defaultSettings $ getPassword Nothing msg 33 | case pwd of 34 | Nothing -> error "You must type a password when prompted!\n" 35 | Just pass -> return (LB8.pack pass) 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Nicolas Schoemaeker 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Nicolas Schoemaeker nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /Types.hs: -------------------------------------------------------------------------------- 1 | module Types ( 2 | PasswordEntry(..) 3 | , LibraryEntry(..) 4 | , PwdHash 5 | , Service 6 | , User 7 | , Pwd 8 | ) where 9 | 10 | import qualified Data.ByteString.Lazy as B (ByteString) 11 | import Data.Csv (FromRecord(..), ToRecord(..), record, toField, (.!)) 12 | import qualified Data.Vector as V (length) 13 | import Data.Functor ((<$>)) 14 | import Control.Applicative ((<*>)) 15 | import Control.Monad (mzero) 16 | 17 | type PwdHash = B.ByteString 18 | type Service = String 19 | type User = String 20 | type Pwd = B.ByteString 21 | 22 | -- Represents an entry inside the library book 23 | data LibraryEntry = LibraryEntry { 24 | getLibraryHash :: PwdHash 25 | , getLibraryFile :: FilePath 26 | } deriving (Eq, Show) 27 | 28 | -- Represents an entry inside an entry book 29 | data PasswordEntry = PasswordEntry { 30 | getService :: Service 31 | , getUser :: User 32 | , getPwd :: Pwd 33 | } deriving (Eq) 34 | 35 | -- The Show instance for PasswordEntry doesn't show the password 36 | instance Show PasswordEntry where 37 | show pwdEntry = "(" ++ getService pwdEntry ++ ", " ++ getUser pwdEntry ++ ")" 38 | 39 | -- ToRecord instance to serialize the password entry into Csv 40 | instance ToRecord PasswordEntry where 41 | toRecord pwdEntry = record [toField (getService pwdEntry), toField (getUser pwdEntry), toField (getPwd pwdEntry)] 42 | 43 | -- FromRecord instance for PasswordEntry to parse Csv 44 | instance FromRecord PasswordEntry where 45 | parseRecord v | V.length v == 3 = PasswordEntry <$> (v .! 0) <*> (v .! 1) <*> (v .! 2) 46 | | otherwise = mzero 47 | 48 | -- ToRecord instance to serialize the library entry into Csv 49 | instance ToRecord LibraryEntry where 50 | toRecord libraryEntry = record [toField (getLibraryHash libraryEntry), toField (getLibraryFile libraryEntry)] 51 | 52 | -- FromRecord instance for LibraryEntry to parse Csv 53 | instance FromRecord LibraryEntry where 54 | parseRecord v | V.length v == 2 = LibraryEntry <$> (v .! 0) <*> (v .! 1) 55 | | otherwise = mzero 56 | -------------------------------------------------------------------------------- /Tools.hs: -------------------------------------------------------------------------------- 1 | module Tools ( 2 | usage 3 | , hpmFolder 4 | , libraryFile 5 | , withLibrary 6 | , withBook 7 | , noEntryBook 8 | , parsingProblem 9 | ) where 10 | 11 | import Data.Functor ((<$>)) 12 | import System.Directory (getHomeDirectory, createDirectoryIfMissing, doesFileExist) 13 | import System.FilePath (()) 14 | 15 | 16 | -- Classic usage command showing commands and their syntax 17 | usage :: String 18 | usage = concat [ "Usage : hpm\t-h, --help : display this message.\n" 19 | , "\t\t-l, --list : list all entries.\n" 20 | , "\t\t-i, --init : initiate a new entry book with a new master password.\n" 21 | , "\t\t-r, --reset : reset your entry book by deleting all stored passwords in it.\n" 22 | , "\t\t-a, --add : add a new password for service with user , the password will be prompted on a secure shell.\n" 23 | , "\t\t-d, --delete : delete the stored password for service .\n" 24 | , "\t\t-e, --extract : extract a password for service .\n" 25 | ] 26 | 27 | -- Error message displayed when no entry book was found 28 | noEntryBook :: String 29 | noEntryBook = "No entry book associated with that master password was found.\n\ 30 | \Initiate a new entry book with --init, -i or consider reading \ 31 | \the help with --help, -h.\n" 32 | 33 | -- Errot to display when 'decode' function fails 34 | parsingProblem :: String 35 | parsingProblem = "There was a problem when parsing your Csv data.\n\ 36 | \Consider resetting your file (all passwords will be lost)." 37 | 38 | -- The hpm folder, containing the library and the entry books 39 | hpmFolder :: IO FilePath 40 | hpmFolder = ( ".hpm") <$> getHomeDirectory 41 | 42 | -- The file containing the master passwords's hashes and the corresponding entry books 43 | libraryFile :: IO FilePath 44 | libraryFile = ( ".booklibary") <$> hpmFolder 45 | 46 | -- Handles the test to verify that the hpm folder and the library file exist to carry out computation 47 | withLibrary :: (FilePath -> IO a) -> IO a 48 | withLibrary f = do 49 | hpmFolder >>= createDirectoryIfMissing True 50 | libraryExists <- libraryFile >>= doesFileExist 51 | if libraryExists then 52 | libraryFile >>= f 53 | else 54 | libraryFile >>= flip writeFile "" >> libraryFile >>= f 55 | 56 | {- Handles the test to verify that the entry book exists to carry out computation. 57 | Argument should be full path name. -} 58 | withBook :: FilePath -> (FilePath -> IO a) -> IO a 59 | withBook book f = do 60 | exists <- doesFileExist book 61 | case exists of 62 | False -> error "The entry book doesn't exist, you may be looking at data corruption." 63 | True -> f book 64 | 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hpm - Haskell Password Manager 2 | ============================== 3 | 4 | hpm is my humble attempt at creating a simple, lightweight password 5 | management application. 6 | hpm is vanilla : there is no fancy add-ons, useless features, 7 | etc. Namely, hpm won't sync with external *data-sucking* servers nor 8 | will it ask you to *Like it* or *Tweet it* or whatever ; sometimes 9 | things should do what they were intended to do, hpm is a password 10 | manager, so it will store and manage your passwords. 11 | 12 | You can start using really long and hard to guess passwords for 13 | everything, as it will hpm's job to remember them for you ! 14 | 15 | Usage 16 | ===== 17 | 18 | Note : 19 | - a service design the entity to which your (user, password) pair is 20 | associated, put whatever you want in it : website name, program name, 21 | etc 22 | - a user is typically the id used to identify, sometimes it is your 23 | username, sometimes it is your email address. 24 | - a password is, well ... your password. As you won't have neither to 25 | remember nor to type it, I suggest you start using *real* passwords 26 | like 'dhh*&4*4"}{++)Jd$£fn' and so on. You can go to 27 | howsecureismypassword.net to have some hints about your password 28 | strength (just be aware that the computing power they estimate is the 29 | one of a single desktop PC, not a multi-million server, so you'd 30 | better divide the number by 1e6 to have a more accurate description) 31 | 32 | 33 | Here's how to use hpm : 34 | ----------------------- 35 | 36 | - create an new entry book and a master password : hpm -i, --init (a 37 | master password is the only password you will have to remember to 38 | manage your passwords, a book is the (encrypted) list of passwords) 39 | - adding a new password : hpm -a, --ass (you will be 40 | prompted for the password you want to add to that service) 41 | - extracting a password for a service : hpm -e, --extract 42 | - deleting a stored password : hpm -d, --delete 43 | - listing your services for which you have a stored password : hpm -l, --list 44 | 45 | Changelog 46 | ========= 47 | 48 | - March 26, 2014 49 | - Project initialization. I am going back to Haskell, and this is a 50 | recover project. Looks awesome. 51 | - Verification of existing ~/.hpm/ folder and create it if non existing 52 | - Asking for master password and checking if a corresponding entry 53 | book is associated 54 | 55 | - March 28, 2014 56 | - Initialization of new library books is working : several books 57 | can be created with different master passwords, and hpm warns the user 58 | if he is trying to create a book with the same master password 59 | - Implemented reset of a new entry book : deletes all passwords in 60 | the entry book associated with his master password 61 | 62 | - March 29, 2014 63 | - Adding a new entry in one's book is working with the limitation 64 | of one password entry per service (this is a choice : so that we can 65 | extract a password with just **hpm -e service** rather than 66 | **hpm -e service user** ; the tool is designed to be *quick* to use) 67 | - Listing and counting stored entries working 68 | - Deleting an entry working (deletion is quiet : it won't warn 69 | your nor will it fail is you try to delete a non-existing entry, in 70 | this case, your book will jsut stay the same, again for speed of use) 71 | - Extracting an entry is working 72 | 73 | - March 31, 2014 74 | - Implemeted SHA512-hashing of master passwords to store the hash 75 | in the book library 76 | 77 | - April 2, 2014 78 | - Removed SHA512-hasing of master passwords as it throws some 79 | problems for now. It doesn't really matter much : the library file is 80 | already encrypted, so the master passwords never appear in plain text 81 | - Implemented hiding of characters when typing passwords 82 | - Now the password is stored in the clipboard when extracted, not 83 | printed on screen anymore 84 | 85 | TODO 86 | ==== 87 | - Rewrite 'initiate' function to use 'withLibrary' rather than calling 88 | 'withLibrary initiate' in the main function 89 | - Rewrite the code in a more elegant fashion : capture the redundant 90 | code between 'list', 'delete' and 'extract' for instance or the 91 | awkward 'Right _' and 'Left _' cases in 'extract' function : I'm 92 | pretty sure it sounds Monad-ish behavior. 93 | - Create a new thread that waits 10 seconds and then erase the 94 | contents of the clipboard when extracting a password. So that the 95 | password doesn't remain in the clipboard. 96 | -------------------------------------------------------------------------------- /Main.hs: -------------------------------------------------------------------------------- 1 | {-#LANGUAGE OverloadedStrings#-} 2 | 3 | module Main ( 4 | main 5 | ) where 6 | 7 | import BookLibrary 8 | import Control.Applicative ((<$>)) 9 | import qualified Data.ByteString.Lazy as B (readFile, writeFile, append) 10 | import qualified Data.ByteString.Lazy.Char8 as B8 (unpack) 11 | import Data.Csv hiding (lookup) 12 | import qualified Data.Vector as V (Vector, toList, length, filter) 13 | import Encrypt 14 | import System.Directory (renameFile) 15 | import System.Environment (getArgs) 16 | import System.IO (openTempFile, hClose) 17 | import System.Process (runCommand) 18 | import Tools 19 | import Types 20 | 21 | main :: IO () 22 | main = getArgs >>= go 23 | where go [] = putStrLn $ "Wrong number of arguments.\n" ++ usage -- no argument : what do to ? 24 | go ("--help":_) = putStrLn usage 25 | go ("-h":_) = go ["--help"] 26 | go ("--list":_) = list 27 | go ("-l":_) = go ["--list"] 28 | go ("--init":_) = withLibrary (initiate Nothing) 29 | go ("-i":_) = go ["--init"] 30 | go ("--reset":_) = reset Nothing 31 | go ("-r":_) = go ["--reset"] 32 | go ("--add":service:user:_) = add service user 33 | go ("-a":service:user:_) = go ["--add", service, user] 34 | go ("--delete":service:_) = delete service 35 | go ("-d":service:_) = go ["--delete", service] 36 | go ("--extract":service:_) = extract service 37 | go ("-e":service:_) = go ["--extract", service] 38 | go _ = putStrLn $ "Error in arguments.\n" ++ usage 39 | 40 | -- Prompt the user for a new master password and create his associated entry book 41 | initiate :: Maybe String -> FilePath -> IO () 42 | initiate Nothing libFile = do 43 | putStrLn $ "You are going to create a new, empty entry book, are you sure you want to continue ? (yes/no)" 44 | answer <- getLine 45 | initiate (Just answer) libFile 46 | initiate (Just "no") _ = putStrLn "Initialization aborded, library book left untouched." 47 | initiate (Just "yes") libFile = do 48 | -- Ask user for his master pwd, check that it doesn't already exist and generate a random filename for it 49 | masterPwd <- askMasterPwd 50 | pwdExist <- libraryLookup masterPwd 51 | case pwdExist of 52 | Just _ -> putStrLn "There already exists a library file associated with that master password.\n\ 53 | \Consider re-using or chose another master password." 54 | Nothing -> do 55 | (filename, h) <- hpmFolder >>= flip openTempFile "bookEntry" 56 | hClose h 57 | let libEntry = encode [LibraryEntry masterPwd filename] 58 | oldDecrypted <- B.readFile libFile >>= return . decrypt 59 | let newDecrypted = oldDecrypted `B.append` libEntry 60 | newEncrypted <- encrypt newDecrypted 61 | (tempName, tempH) <- hpmFolder >>= flip openTempFile ".tempLibrary" 62 | hClose tempH 63 | B.writeFile tempName newEncrypted 64 | renameFile tempName libFile 65 | putStrLn "New library book initiated. You can start storing passwords now." 66 | 67 | initiate _ libFile = putStrLn "Answer with \"yes\" or \"no\"." >> initiate Nothing libFile 68 | 69 | -- Lookup generalized for the library book to be used with 'withLibrary' 70 | libraryLookup :: PwdHash -> IO (Maybe FilePath) 71 | libraryLookup pwd = withLibrary $ \libFile -> do 72 | libraryRaw <- B.readFile libFile >>= return . decrypt 73 | let libraryDec = decode NoHeader libraryRaw :: Either String (V.Vector LibraryEntry) 74 | case libraryDec of 75 | Left _ -> error $ "Error while trying to read the library, file may be corrupted\n" 76 | Right library -> return $ entryLookup pwd (V.toList library) 77 | 78 | -- Lookup generalized for the entry book. 79 | bookLookup :: FilePath -> Service -> IO (Maybe Pwd) 80 | bookLookup book serv = do 81 | entries <- decode NoHeader <$> (B.readFile book >>= return . decrypt) 82 | case entries of 83 | Left _ -> error parsingProblem 84 | Right entries' -> return $ bookLookup' serv (V.toList entries') 85 | 86 | bookLookup' :: Service -> [PasswordEntry] -> Maybe Pwd 87 | bookLookup' _ [] = Nothing 88 | bookLookup' serv (x:xs) = if serv == getService x then 89 | Just (getPwd x) 90 | else 91 | bookLookup' serv xs 92 | 93 | -- lookup like function that applies to LibraryEntry data type 94 | entryLookup :: PwdHash -> [LibraryEntry] -> Maybe FilePath 95 | entryLookup _ [] = Nothing 96 | entryLookup pwdHash (x:xs) = if (getLibraryHash x) == pwdHash then 97 | Just (getLibraryFile x) 98 | else 99 | entryLookup pwdHash xs 100 | 101 | -- Prompt for master password and delete all stored passwords in an entry book 102 | reset :: Maybe String -> IO () 103 | reset Nothing = do 104 | putStrLn "You are about to delete all stored passwords in your entry book, are you sure ?(yes/no)" 105 | answer <- getLine 106 | reset (Just answer) 107 | reset (Just "no") = putStrLn "Reset aborded, entry book left untouched." 108 | reset (Just "yes") = do 109 | masterPwd <- askMasterPwd 110 | bookEntry <- libraryLookup masterPwd 111 | go bookEntry 112 | where go Nothing = putStrLn noEntryBook 113 | go (Just entryBook) = do 114 | encrypt "" >>= B.writeFile entryBook -- write empty string in file to erase its contents 115 | putStrLn "Entry book was erased successfully.\n" 116 | reset _ = putStrLn "Please answer with \"yes\" or \"no\".\n" >> reset Nothing 117 | 118 | -- Prompt for master password and store the new entry in the associated entry book 119 | add :: Service -> User -> IO () 120 | add [] _ = putStrLn $ "Error in arguments.\n" ++ usage 121 | add _ [] = add [] [] -- calls above 122 | add service user = 123 | askPassword (Just "Type your MASTER password to access your entry book.\n") >>= libraryLookup >>= go 124 | where go Nothing = putStrLn noEntryBook 125 | go (Just entryBook) = do 126 | -- Check that there is not already a password entry for that service 127 | exists <- withBook entryBook (flip bookLookup service) 128 | case exists of 129 | Just _ -> putStrLn $ "There already exists a password entry for service \"" ++ service ++ "\" in your book." 130 | Nothing -> do 131 | pwd <- askPassword (Just $ "Type the password you want to associate to service \"" ++ service ++ "\" and user \"" ++ user ++ "\": ") 132 | let newEntry = encode [PasswordEntry service user pwd] 133 | oldDecrypted <- B.readFile entryBook >>= return . decrypt 134 | newEncrypted <- encrypt $ oldDecrypted `B.append` newEntry 135 | (tempName, tempH) <- hpmFolder >>= flip openTempFile "tempEntryBook" 136 | hClose tempH 137 | B.writeFile tempName newEncrypted 138 | renameFile tempName entryBook 139 | putStrLn "New password entry added to entry book." 140 | 141 | 142 | -- List password entries from an entry book 143 | list :: IO () 144 | list = do 145 | exists <- askMasterPwd >>= libraryLookup 146 | case exists of 147 | Nothing -> putStrLn noEntryBook 148 | Just entryBook -> do 149 | entries <- (decode NoHeader <$> (B.readFile entryBook >>= return . decrypt)) :: IO (Either String (V.Vector PasswordEntry)) 150 | case entries of 151 | Left _ -> putStrLn parsingProblem 152 | Right entries' -> putStrLn ("\nShowing (" ++ show (V.length entries') ++ ") entries :") 153 | >> mapM_ (putStrLn . (" > " ++) . show) (V.toList entries') 154 | 155 | -- Deletes a password entry associated to the service 156 | delete :: Service -> IO () 157 | delete service = do 158 | exists <- askMasterPwd >>= libraryLookup 159 | case exists of 160 | Nothing -> putStrLn noEntryBook 161 | Just entryBook -> do 162 | entries <- (decode NoHeader <$> (B.readFile entryBook >>= return . decrypt)) :: IO (Either String (V.Vector PasswordEntry)) 163 | case entries of 164 | Left _ -> putStrLn parsingProblem 165 | Right entries' -> do 166 | let newEntries = flip V.filter entries' (\e -> service /= getService e) 167 | encrypt (encode $ V.toList newEntries) >>= B.writeFile entryBook 168 | putStrLn "Password entry removed if it existed.\n" 169 | 170 | -- Extracts a password associated to the service 171 | 172 | extract :: Service -> IO () 173 | extract service = do 174 | exists <- askMasterPwd >>= libraryLookup 175 | case exists of 176 | Nothing -> putStrLn noEntryBook 177 | Just entryBook -> do 178 | entries <- (decode NoHeader <$> (B.readFile entryBook >>= return . decrypt)) :: IO (Either String (V.Vector PasswordEntry)) 179 | case entries of 180 | Left _ -> putStrLn parsingProblem 181 | Right _ -> do 182 | pass <- bookLookup entryBook service 183 | case pass of 184 | Nothing -> putStrLn $ "No password entry for service \"" ++ service ++ "\" found in your entry book." 185 | Just pass' -> do 186 | let passStr = B8.unpack pass' 187 | runCommand ("echo " ++ passStr ++ " | xclip -i") >> putStrLn "Password now in clipboard." 188 | --------------------------------------------------------------------------------