module Pwqgen

Ruby implementation of Openwall passwdqc

vim: set expandtab sw=2 ts=2:ft=ruby

vim: set expandtab sw=2 ts=2:ft=ruby

Constants

NUMERIC_SEPARATORS

numeric separators

SEPARATORS

default separators

VERSION

version number

WORDSET

This list has been downcased, since passwdqc pwqgen can uncapitalize already capitalized words

comment directly from passwdqc:

4096 English words for generation of easy to memorize random passphrases. This list comes from the MakePass passphrase generator developed by Dianelos Georgoudis <dianelos at tecapro.com>, which was announced on sci.crypt on 1997/10/24. Here's a relevant excerpt from that posting:

The 4096 words in the word list were chosen according to the following criteria:

  • each word must contain between 3 and 6 characters

  • each word must be a common English word

  • each word should be clearly different from each other word, orthographically or semantically

The MakePass word list has been placed in the public domain

At least two other sci.crypt postings by Dianelos Georgoudis also state that the word list is in the public domain, and so did the web page at:

web.archive.org/web/%2a/http://www.tecapro.com/makepass.html

which existed until 2006 and is available from the Wayback Machine as of this writing (March 2010). Specifically, the web page said:

> The MakePass word list has been placed in the public domain. To download > a copy click here. You can use the MakePass word list for many other > purposes.

“To download a copy click here” was a link to free/makepass.lst, which is currently available via the Wayback Machine:

web.archive.org/web/%2a/http://www.tecapro.com/free/makepass.lst

Even though the original description of the list stated that “each word must contain between 3 and 6 characters”, there were two 7-character words: “England” and “Germany”. For use in passwdqc, these have been replaced with “erase” and “gag”.

The code in passwdqc_check.c and passwdqc_random.c makes the following assumptions about this list:

  • there are exactly 4096 words;

  • the words are of up to 6 characters long;

  • although some words may contain capital letters, no two words differ by

the case of characters alone (e.g., converting the list to all-lowercase would yield a list of 4096 unique words);

  • the words contain alphabetical characters only;

  • if an entire word on this list matches the initial substring of other

word(s) on the list, it is placed immediately before those words (e.g., “bake”, “baker”, “bakery”).

Additionally, the default minimum passphrase length of 11 characters specified in passwdqc_parse.c has been chosen such that a passphrase consisting of any three words from this list with two separator characters will pass the minimum length check. In other words, this default assumes that no word is shorter than 3 characters.

Public Class Methods

log2(n) click to toggle source

if n is a power of 2, return log2(n), else return nil

Arguments:

n: (Integer)
# File lib/pwqgen/pwqgen.rb, line 71
def log2(n)
  return nil if n <= 0

  # return nil unless n is a power of 2
  return nil unless (n & (n - 1)).zero?

  bits = 0

  while n > 255
    n >>= 8
    bits += 8
  end

  while n > 0
    n >>= 1
    bits += 1
  end
  bits - 1
end
pwqgen( n_words:, random_generator: proc { |x| Sysrandom.random_bytes(x) }, separators: SEPARATORS, random_capitalize: true ) click to toggle source

Pwqgen.pwqgen - generate a random passphrase using the pwqgen algorithm

Arguments:

n_words: (Integer)
random_generator: (Proc) - Proc or method reference - this should take one argument, n, and return n random bytes
  as a String of length n. Default is to use Sysrandom.random_bytes
separators: (String) - separators for use between words. Default is Pwqgen::SEPARATORS. Array of one character strings
random_capitalize: (Boolean) - whether or not to "randomly" capitalize words. Default is true.

Examples:

require 'pwqgen'
require 'securerandom'  # only for the second and third examples

# Five words. Default behaviour
puts Pwqgen.pwqgen(n_words: 5)

# use Securerandom instead of Sysrandom and with custom separators
puts Pwqgen.pwqgen(n_words: 5,
random_generator: SecureRandom.method(:random_bytes),
random_capitalize: false,
separators: %w(2 3 4 |)
)
# OR
puts Pwqgen.pwqgen(n_words: 5,
random_generator: proc { |x| SecureRandom.random_bytes(x) },
random_capitalize: false,
separators: %w(2 3 4 |)
)

# produces "adam-adam-adam-adam" as the random generator always returns 0
puts Pwqgen.pwqgen(n_words: 4, random_generator: Proc.new {|x| "\000" * x }})
# File lib/pwqgen/pwqgen.rb, line 44
def pwqgen(
    n_words:,
    random_generator:       proc { |x| Sysrandom.random_bytes(x) },
    separators:             SEPARATORS,
    random_capitalize:      true
)
  # validate arguments
  raise ArgumentError, "n_words must be an integer: #{n_words.inspect}" unless n_words.is_a? Integer
  raise ArgumentError, "n_words must be a positive integer: #{n_words.inspect}" if n_words < 1
  raise ArgumentError, "invalid random_generator: #{random_generator.inspect}" unless random_generator.respond_to? :call
  raise ArgumentError, "separators must be Array of length 2**n n<=12, of one characters strings: #{separators.inspect}" \
    unless (separators.is_a? Array) && separators.inject(true) { |a, e| a && (e.is_a? String) && e.length == 1 } \
    && log2(separators.length)

  random_separators = Array.new(n_words - 1) { random_separator(random_generator: random_generator, separators: separators) }
  random_words = Array.new(n_words) { random_word(random_generator: random_generator, random_capitalize: random_capitalize) }

  # interleave random_words and random_separators. Relies on nil.to_s is empty string
  random_words.inject('') do |a, e|
    a + e + random_separators.shift.to_s
  end
end

Private Class Methods

random_bits(random_generator:, n_bits:) click to toggle source

Get n_bits bits from the random generator. This squanders some randomness, since it gets the required number of bytes and throws away bits that are not needed. Returns an Integer between 0 and 2**n_bits - 1. Arguments:

random_generator: (Proc) Proc or method reference for random_bytes
n_bits: (Integer)
# File lib/pwqgen/pwqgen.rb, line 99
def random_bits(random_generator:, n_bits:)
  bits = 0
  random_generator.call(((n_bits - 1) / 8) + 1).unpack('C*').each_with_index { |byte, i| bits |= byte << (8 * i) }
  bits & ((1 << n_bits) - 1) # throw away the bits we do not need
end
random_separator(random_generator:, separators:) click to toggle source

generate a separator “randomly” chosen from separators

# File lib/pwqgen/pwqgen.rb, line 114
def random_separator(random_generator:, separators:)
  separators[random_bits(random_generator: random_generator, n_bits: log2(separators.length))]
end