A while back I wrote about Diceware, a system for generating password using dice and a word list. I also include a Ruby script that use virtual dice.

The diceware passwords of reasonable length are strong, they have high entropy. However, most password security requirements aren’t based on entropy, but instead are made up, with people throwing in requirements for capital letters, numbers, and symbols because they sound good.

Well, we don’t get to set policy, so I’ve modified my script to generate passwords with numbers and symbols as separators. I’m using the Diceware method which lays these characters out in a grid and then uses two rolls generate coordinates in the grid:

SYMBOLS = [%w[1 ~ ! # $ % ^],
           %w[2 & * ( ) - =],
           %w[3 + [ ] \ { }],
           %w[4 : ; " ' < >],
           %w[5 ? / 0 1 2 3],
           %w[6 4 5 6 7 8 9]]
x,y = rand(6), rand(6)
separator = SYMBOLS[x][y]

You could make that simpler with:

separator = SYMBOLS.sample.sample

but I’m sticking with the original dice concept.

If we have count words, we need count-1 separators, which we can get with:

separators = (count.to_i - 1).times.map { SYMBOLS[rand(6)][rand(6)] }

Once we have words and separators we need to interleave them together. This is a job for Array#zip

%w[one two three].zip([1,2])
 => [["one", 1], ["two", 2], ["three", nil]]

Because zip returns an array of arrays when need to flatten it out.

[["one", 1], ["two", 2], ["three", nil]].flatten
 => ["one", 1, "two", 2, "three", nil]

and join them:

["one", 1, "two", 2, "three", nil].join
 => "one1two2three"

Throw in a option to toggle separators v.s. spaces, and the final results is:

#!/usr/bin/env ruby

require 'optparse'

dice_file = '/Users/spike/Documents/diceware.wordlist.asc.asc'

SYMBOLS = [%w[1 ~ ! # $ % ^],
           %w[2 & * ( ) - =],
           %w[3 + [ ] \ { }],
           %w[4 : ; " ' < >],
           %w[5 ? / 0 1 2 3],
           %w[6 4 5 6 7 8 9]]

alphanumeric = false

OptionParser.new do |opts|
  opts.banner = "Usage: diceware [-s] [word-count]"

  opts.on("-s", "--separators", "Use alphanumeric separators instead of spaces") do
    alphanumeric = true
  end
end.parse!

count = ARGV[0] || 2

if alphanumeric
  separators = (count.to_i - 1).times.map { SYMBOLS[rand(6)][rand(6)] }
else
  separators = [' '] * (count.to_i - 1)
end

words = {}
f = File.open(dice_file, "r")
f.each_line do |line|
  next unless (line =~ /^\d\d\d\d\d/)
  key,word = line.split
  words[key] = word
end

passwords = count.to_i.times.map do
  roll = (0..4).map{ 1 + rand(6) }.join
  words[roll]
end

puts passwords.zip(separators).flatten.join

Disclaimer, this is a secure approach, but you can only trust random number generators so far. If you want a really secure approach break out the physical dice and do this with pen and paper.

Dice image some rights reserved by eclesh.

Comments