-- Copyright 2016 Marcelo Garlet Millani
-- This file is part of paphragen.
-- paphragen is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
-- paphragen is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
-- You should have received a copy of the GNU General Public License
-- along with paphragen. If not, see .
module Main where
import Data.Char
import Data.List
import qualified Data.ByteString.Lazy as BS
import qualified Data.Word as W
import qualified Data.Bits as B
import qualified Data.Map.Strict as Map
import System.Environment
import System.IO
appname = "paphragen"
appversion = "0.2.0.0"
data Command = Help | Build | Generate deriving(Eq, Show, Ord)
data Action =
Action
{ cmd :: Command
, outFile :: FilePath
, passLength :: Int
, passEntropy :: Int
, inputFiles :: [FilePath]
, infoFun :: String -> IO ()
}
defaultAction =
Action
{ cmd = Help
, outFile = ""
, passLength = 0
, passEntropy = 100
, inputFiles = []
, infoFun = putStrLn
}
quiet :: String -> IO ()
quiet _ = return ()
-- | Splits a text into words.
wordSplit [] = []
wordSplit text =
(map toLower word) : (wordSplit rest)
where
(word, text') = span isLetter text
rest = dropWhile (not . isLetter) text'
-- | Writes a dictionary to a file. The file will have one word per line in ascending alphabetical order.
writeDictionary dict handle = do
mapM_ (hPutStrLn handle) $ map fst $ Map.toAscList dict
-- | Loads a dictionary from a file.
loadDictionary fl = do
text <- readFile fl
return $ lines text
-- | Insert words into a dictionary, increasing their count if they already exist.
insertWords dict text = foldl (\d w -> Map.insertWith (+) w 1 d) dict $ wordSplit text
-- | Reads a dictionary from a file.
readDictionary dict [] = return dict
readDictionary dict (f:fs) = do
text <- readFile f
readDictionary (insertWords dict text) fs
-- | Builds a dictionary form a list of files, writing the output to the given handle.
buildDictionary files out = do
rawDict <- readDictionary Map.empty files
let dictionary = Map.filterWithKey (\w c -> length w > 1 && length w < 8) $ rawDict
(distinct, occurrences) = Map.foldl (\(d,o) y -> (d+1, o+y)) (0,0) dictionary
threshold = occurrences `div` (distinct * 3)
common = Map.filter (>= threshold) dictionary
writeDictionary common out
-- | Take elements from a list with the given indices. The first component of the tuple is the index on the list. The second element is preserved.
-- | This is used to draw many words from a list without having to iterate through it multiple times.
takeIndices is ds =
takeIndices' 0 is ds
where
takeIndices' k [] ds = []
takeIndices' k ((x,i):is) ds = (head rs, i) : takeIndices' x is rs
where
rs = drop (x-k) ds
-- | Generates indices for a set of n elements using r as a source of randomness
randomIndices n rs
| x < n = x : randomIndices n (BS.drop bytes rs)
| otherwise = randomIndices n (BS.drop bytes rs)
where
entropy = ceiling $ logBase 2 (fromIntegral n)
bytes = ceiling $ logBase 8 (fromIntegral n)
mask = 2^entropy - 1
x = mask B..&. (fromIntegral $ BS.foldl accum (0 :: W.Word) (BS.take bytes rs))
accum b x = (B.shift b 8) B..|. (fromIntegral x)
-- | Prints help text.
help = do
mapM_ putStrLn [
appname ++ " " ++ appversion
, "A passphrase generator."
, "usage:"
, "To build a dictionary based on the words of input files:"
, "\t" ++ appname ++ " build [OPTIONS...] "
, "\t where OPTIONS are:"
, "\t\t-o, --output DICTIONARY writes output to DICTIONARY instead of stdout."
, "\nTo generate a password using an existing dictionary:"
, "\t" ++ appname ++ " generate [OPTIONS...] "
, "\t where OPTIONS are:"
, "\t\t-e, --entropy N sets the minimum desired entropy (default: " ++ show (passEntropy defaultAction) ++ " bits)."
, "\t\t-l, --length N number of words to use (entropy is used by default)."
, "\tIn this case, a random sequence of bytes should be provided through stdin."
, "\tOn Unix-like systems, /dev/random is a good choice."
]
parseArgs action args
| cmd action == Build = case args of
"-o":file:rs -> parseArgs action{outFile = file} rs
"--output":file:rs -> parseArgs action{outFile = file} rs
rs -> action{inputFiles = rs}
| cmd action == Generate = case args of
"-l":len:rs -> parseArgs action{passLength = read len} rs
"--length":len:rs -> parseArgs action{passLength = read len} rs
"-e":ent:rs -> parseArgs action{passEntropy = read ent} rs
"--entropy":ent:rs -> parseArgs action{passEntropy = read ent} rs
"-q":rs -> parseArgs action{infoFun = quiet} rs
"--quiet":rs -> parseArgs action{infoFun = quiet} rs
rs -> action{inputFiles = rs}
| cmd action == Help = case args of
"build":rs -> parseArgs action{cmd = Build} rs
"generate":rs -> parseArgs action{cmd = Generate} rs
rs -> parseOptions action rs
parseOptions action args = case args of
"-o":file:rs -> parseArgs action{outFile = file} rs
"--output":file:rs -> parseArgs action{outFile = file} rs
"-l":len:rs -> parseArgs action{passLength = read len} rs
"--length":len:rs -> parseArgs action{passLength = read len} rs
"-e":ent:rs -> parseArgs action{passEntropy = read ent} rs
"--entropy":ent:rs -> parseArgs action{passEntropy = read ent} rs
"-q":rs -> parseArgs action{infoFun = quiet} rs
"--quiet":rs -> parseArgs action{infoFun = quiet} rs
_ -> action{cmd = Help}
execute action
| cmd action == Build = execBuild action
| cmd action == Generate = execGenerate action
| cmd action == Help = help
execBuild action
| null $ outFile action = do
buildDictionary (inputFiles action) stdout
| otherwise = do
withFile (outFile action) WriteMode (buildDictionary (inputFiles action))
putStrLn $ "Common words written to " ++ (outFile action)
execGenerate action
| null $ inputFiles action = execute action{cmd = Help}
| otherwise = do
dict <- (mapM loadDictionary $ inputFiles action) >>= (return . concat)
let n = length dict
(infoFun action) $ "Dictionary has " ++ show n ++ " words."
let epw = logBase 2 (fromIntegral n)
(infoFun action) $ "This gives an entropy of " ++ show epw ++ " bits per word."
let k = if passLength action == 0 then ceiling $ (fromIntegral $ passEntropy action) / epw else passLength action
(infoFun action) $ "Generating a password with " ++ show k ++ " words (" ++ show ((fromIntegral k) * epw) ++ " bits of entropy)."
randomness <- BS.getContents
let indices = zip (take k $ randomIndices n randomness) [0..]
sInd = sortBy (\x y -> (fst x) `compare` (fst y)) indices
pickedWords = sortBy (\x y -> (snd x) `compare` (snd y)) $ takeIndices sInd dict
mapM_ (\w -> putStr $ " " ++ fst w) pickedWords
putStr "\n"
main :: IO ()
main = do
args <- getArgs
let action = parseArgs defaultAction args
execute action