Mathias Jean Johansen
Building a Passphrase Generator in Haskell
During the recent holiday season, I decided to put my admittedly limited Haskell skills to use. While the CIS 194 course exercises are excellent for learning and practicing Haskell, and I enjoy working through them when time permits, I wanted to work on a more structured “real world” project this time.
I had a few goals in mind for this particular project. First and foremost, it had to be a well-defined and relatively simple problem that I needed to solve. Secondly, I wanted it to require me to handle IO and side effects, and, lastly, I wanted it to offer me an opportunity to integrate third-party libraries in my app.
Only a couple of weeks prior to starting this project, I had been reading the guide by the Electronic Frontier Foundation on how to generate strong six-word Diceware passphrases. Turning their suggested method into a small command-line utility seemed to suit my three goals nicely, so I decided to give it the old college try. In this blog post, I will elaborate on how this method of generating passphrases works and how it can be implemented in Haskell. The entire project can be found on GitHub here.
The steps needed to generate a passphrase using the directions provided by EFF are fairly simple, and you only need five dice and a wordlist:
- Roll five dice at the same time and write down the five numbers. The numbers might be something like 4, 3, 4, 6, 3.
- In your wordlist, find the word that matches the number 43463 (in this case, panoramic), and write it down.
- Repeat these two steps until you have (at least) six words. Once you have repeated the steps, you might have a passphrase such as “whinny daffodil aerobics upheld bankable niece.”
Before detailing the implementation, it should be noted that the project uses rio
as a prelude replacement by setting {-# LANGUAGE NoImplicitPrelude #-}
and importing RIO
in all files. With our project description in place, we are now ready to implement it.
In order to solve the first step, we need to be able to roll five dice and join the numbers into a single digit. To do so, we define a rollDice
function:
rollDice :: Int -> Integer -> Integer -> IO [Integer]
rollDice rolls start end = replicateM rolls $ generateBetween start end
rollDice
takes three arguments: rolls
which describes how many dice we want to roll, and start
and end
which respectively describe the start and end of the range of numbers, we will be working with. Here, we rely on generateBetween
from the Cryptonite library to generate a random number within our range. Since our result is wrapped in a monad, we need to use replicateM
to repeat this process N times.
Next, we need to be able to join these digits into one. To do this, we will define a function joinDigits
in the following manner:
joinDigits :: [Integer] -> Integer
joinDigits = L.foldl' ((+) . (* 10)) 0
This function takes a list of Integer
s and uses L.foldl'
to combine them into one.
For every element in the list, we multiply our accumulator with 10 and then add the first element in our list to the result. This process is repeated until our list is empty. For instance, given the numbers 5, 2, 3, 1, 6, and 0 as our initial accumulator value, this is how our result is calculated:
0 # Initial accumulator value
0*10+5 = 5 # The list is now [2, 3, 1, 6]
5*10+2 = 52 # The list is now [3, 1, 6]
52*10+3 = 523 # The list is now [1, 6]
523*10+1 = 5231 # The list is now [6]
5231*10+6 = 52316 # The list is now []
With these two functions defined, we now have the necessary parts for generating the indices which we will need later when pairing dice rolls with words. To pair indices with words, we will need a function toTuple
:
toTuple :: [String] -> (Integer, String)
toTuple [] = (0, "")
toTuple (index:word) = (fromMaybe 0 index', fromMaybe "" word')
where
index' = readMaybe index :: Maybe Integer
word' = L.headMaybe word
toTuple
takes a list of String
s and returns a tuple of (Integer, String)
where the Integer
represents an index, and the String
represents the corresponding word from the wordlist. We will call this function later when we map over each line in our wordlist.
Since a line in our wordlist follows the format “11111 abacus
”, we want to read the first string as an Integer
and take the first word of the remaining words in our line. As read
and head
can throw exceptions, we use the safe alternatives readMaybe
and L.headMaybe
instead. To unwrap our Maybe
values, we call fromMaybe
and use 0
and ""
as the default values for the index and word, respectively. Lastly, to avoid non-exhaustive patterns, we pattern match on the empty list and return (0, "")
for good measure.
Now we can finally move on to writing the logic for generating passphrases which is handled by the passphrase
function:
passphrase :: String -> [[Integer]] -> String
passphrase wordlist dice =
unwords $ map (flip (Map.findWithDefault "") wordMap) indices
where
indices = map joinDigits dice
wordMap = Map.fromList $ map (toTuple . words) $ lines wordlist
To generate a passphrase, we need to pass our wordlist as the first argument and then a list containing a list of our dice rolls as the second argument. The first argument will look like this:
11111 abacus
11112 abdomen
11113 abdominal
11114 abide
11115 abiding
11116 ability
11121 ablaze
11122 able
11123 abnormal
11124 abrasion
11125 abrasive
11126 abreast
11131 abridge
...
While the second argument, i.e., our dice rolls, might look like this:
[ [5, 3, 2, 1, 6]
, [3, 4, 1, 6, 6]
, [2, 4, 4, 1, 3]
, [1, 3, 3, 5, 4]
, [2, 3, 2, 1, 5]
, [6, 4, 3, 2, 5]
]
For all the lists in our list of dice rolls, we use our previously defined joinDigits
function to join the numbers in the list into single digits. We then store these digits, or indices, in the indices
variable, so we can use them later when we need to look up words in our wordlist.
As shown above, the wordlist follows the format INDEX WORD
, so we split our wordlist on line breaks using lines
which gives us a list of String
s containing elements such as "11111 abacus"
and "11112 abdomen"
. We want to further break these elements down so we will end up with a list of tuples. To do so, we map over all the lines in our wordlist and call (toTuple . words)
on them. Now that we have a list of tuples, we can convert it to a Map
using Map.fromList
so we can perform efficient lookups later.
For all our indices, we want to find the corresponding word in our wordlist which we do with map (flip (Map.findWithDefault "") wordMap) indices
. Since Map.findWithDefault
expects the index of the map to be the second argument and the map to be the last, we use two useful techniques here to make our code less convoluted. First, we leverage currying by applying ""
as the first argument to Map.findWithDefault
, and then we use flip
to reverse the order of the last two arguments.
By flipping the order, we can now do a simple map over our indices
variable and perform a lookup in our wordMap
in a slightly more elegant fashion. Once we have all our words, we can join them into a single String
using unwords
.
With the fundamental logic now defined, we only need to read our wordlist from a file, throw five dice six times, and pass these two values to our passphrase
function. We do so by defining the entry point of our app in Main.hs
as such:
main :: IO ()
main = do
wordlist <- T.unpack <$> readFileUtf8 "data/eff-large-wordlist.txt"
dice <- replicateM 6 $ rollDice 5 1 6
runSimpleApp $ do
logInfo . display . T.pack $ passphrase wordlist dice
This largely resembles the steps listed earlier. To retrieve our wordlist, we call readFileUtf8
with the path to our wordlist, and then T.unpack
to convert the Text
value into a String
.
Next, we roll our five dice six times by calling replicateM 6 $ rollDice 5 1 6
, and pass our wordlist
and dice
to the passphrase
function.
After calling our passphrase
function, we now have a passphrase that we need to output in our terminal. Following the conventional structure for how to build apps using rio
, we run our app as a SimpleApp
which provides a convenient default configuration. We convert the result from our passphrase
function into a Text
value, then a Utf8Builder
, and, lastly, print it in our terminal with logInfo
.
With everything in place, we have now reached our goal and can finally build our project by running stack build
and generate a passphrase:
% stack exec -- passphrase
polar geek nimble okay appealing trim
In my experience, the Haskell community is great at sharing their knowledge on both Twitter and in various blog posts. With this blog post, I hope to contribute back to the community and help especially the people newer to the language who might want to move beyond contrived exercises and learn how to build smaller apps. If you are interested, you can find the project on GitHub here, and if you have any feedback, I am all ears.