A clojure kata: Diceware password generator (part III)

Ok, let’s finish this

In our first chapter we created a function called generate-key that works like this

user=> (generate-key)
33251
user=> (generate-key)
21414
user=> (generate-key)
55361

It generates a random 5 digit number where all the digits are between 1 and 6.

In the second chapter we created a var called wordlist-map. It is a delay that pulls down the diceware word list and stores it in a map.

user=> (@wordlist-map 33251)
"hop"
user=> (@wordlist-map 21414)
"corey"
user=> (@wordlist-map 55361)
"stab"

Let’s combine these two things into a function.

(defn get-random-word []
  (get @wordlist-map (generate-key)))

A single word isn’t good enough for a password. We need multiple words. How many though? In this case we should probably allow the function caller to decide. We’ll make it a parameter.

(defn generate-passphrase [word-count]
  (repeatedly word-count get-random-word))

Try it

user=> (generate-passphrase 2)
("romp" "light")
user=> (generate-passphrase 3)
("finny" "fh" "cruel")
user=> (generate-passphrase 4)
("year" "tipsy" "blvd" "rapt")
user=> (generate-passphrase 5)
("stoop" "xh" "1955" "bane" "usps")
user=> (generate-passphrase 10)
("claus" "betsy" "angola" "align" "abuse" "macon" "filly" "quash" "luck" "hug")

That about covers it. The only other thing we might want to do is create a -main function so we can generate a java jar file and run it from a command line console. Here’s one way to do that.

(defn -main
  ([] (-main 5))
  ([n] (let [n (if (integer? n) n
                   (try (Integer/parseInt n)
                        (catch Exception e 5)))]
         (println (generate-passphrase n)))))

This one has two different ways to call it. When you supply an input parameter it checks to see if it is an integer. If it is, it generates a passphrase that is n words long. If it isn’t or if the value can’t be parsed as an integer or if you don’t supply a parameter at all, it generates a passphrase of 5 words.

That’s it. For completeness let’s print out the entire namespace

(ns katas.diceware
  (:gen-class)
  (:require [clojure.java.io :as io]))

(defn roll-die []
  (inc (rand-int 6)))

(defn generate-key []
  (let [roll-results (repeatedly roll-die)
        exponents (map (fn [n] (int (Math/pow 10 n))) 
                       (reverse (range 0 5)))]
    (reduce + (map * roll-results exponents))))

(def wordlist-url 
  (java.net.URL. "http://world.std.com/~reinhold/diceware.wordlist.asc"))

(defn get-wordlist []
  (filter (fn [line] (re-matches #"[1-6]{5}.*" line)) 
          (line-seq (io/reader wordlist-url))))

(defn split-line [line]
  (let [[_ k-string val] (first (re-seq #"([1-6]{5}).(.*)" line))]
    [(Integer/parseInt k-string) val]))

(def wordlist-map (delay (into {} (map split-line (get-wordlist)))))

(defn get-random-word []
  (get @wordlist-map (generate-key)))

(defn generate-passphrase [word-count]
  (repeatedly word-count get-random-word))

(defn -main
  ([] (-main 5))
  ([n] (let [n (if (integer? n) n
                   (try (Integer/parseInt n)
                        (catch Exception e 5)))]
         (println (generate-passphrase n)))))

That’s 37 lines of kata goodness. There are a couple of enhancements we could try.

  • The site recommends that if the passphrase length is less than 17 characters you shouldn’t use it. We could add code that makes sure this is true.
  • The word list has a digital signature. We could add some code to verify that signature.
  • The diceware site also has a table for picking random non-alphanumeric characters which some password policies require. We could add this in.

We’ll leave these excellent enhancements as exercises.