Contents

DiceWare Password Generator: Cryptographically Secure Password Generation

DiceWare Password Generator: Cryptographically Secure Password Generation

In this post, I’ll share insights from my DiceWare Password Generator project, which implements the DiceWare method for generating cryptographically secure passwords using Ruby, demonstrating advanced security practices and random number generation techniques.

Project Overview

The DiceWare Password Generator is a Ruby implementation of the DiceWare method, a technique for generating secure passwords using dice rolls and word lists. This project showcases cryptographic security principles, entropy generation, and integration with authentication systems like OpenLDAP.

Technical Architecture

Project Structure

DiceWarePasswordGenerator/
├── lib/
│   ├── diceware/
│   │   ├── generator.rb
│   │   ├── word_list.rb
│   │   ├── entropy.rb
│   │   └── validator.rb
│   ├── crypto/
│   │   ├── random_generator.rb
│   │   ├── hash_functions.rb
│   │   └── encryption.rb
│   └── integration/
│       ├── ldap_client.rb
│       ├── password_manager.rb
│       └── user_interface.rb
├── data/
│   ├── wordlists/
│   │   ├── diceware.txt
│   │   ├── eff_large.txt
│   │   └── custom.txt
│   └── config/
│       ├── settings.yml
│       └── ldap_config.yml
├── tests/
│   ├── unit/
│   ├── integration/
│   └── fixtures/
├── bin/
│   ├── diceware
│   └── password_manager
├── Gemfile
├── Rakefile
└── README.md

Core Implementation

DiceWare Generator

# lib/diceware/generator.rb
require 'securerandom'
require 'digest'
require_relative 'word_list'
require_relative 'entropy'
require_relative 'validator'

module DiceWare
  class Generator
    attr_reader :word_list, :entropy_calculator, :validator
    
    def initialize(word_list_path = nil, options = {})
      @word_list = WordList.new(word_list_path)
      @entropy_calculator = Entropy.new
      @validator = Validator.new
      @options = default_options.merge(options)
    end
    
    def generate_password(word_count = 6, separator = ' ')
      validate_inputs(word_count, separator)
      
      words = generate_words(word_count)
      password = words.join(separator)
      
      {
        password: password,
        words: words,
        entropy: calculate_entropy(word_count),
        strength: assess_strength(password),
        generated_at: Time.now
      }
    end
    
    def generate_passphrase(word_count = 6, separator = ' ', include_numbers = false, include_symbols = false)
      validate_inputs(word_count, separator)
      
      words = generate_words(word_count)
      
      # Add numbers if requested
      if include_numbers
        numbers = generate_numbers(word_count)
        words = words.zip(numbers).flatten.compact
      end
      
      # Add symbols if requested
      if include_symbols
        symbols = generate_symbols(word_count)
        words = words.zip(symbols).flatten.compact
      end
      
      password = words.join(separator)
      
      {
        password: password,
        words: words,
        entropy: calculate_entropy(word_count, include_numbers, include_symbols),
        strength: assess_strength(password),
        generated_at: Time.now
      }
    end
    
    def generate_secure_password(length = 16, character_set = :all)
      validate_password_length(length)
      
      chars = get_character_set(character_set)
      password = generate_random_string(length, chars)
      
      {
        password: password,
        entropy: calculate_entropy_for_chars(length, chars.length),
        strength: assess_strength(password),
        generated_at: Time.now
      }
    end
    
    def batch_generate(count = 10, word_count = 6)
      passwords = []
      
      count.times do
        passwords << generate_password(word_count)
      end
      
      passwords
    end
    
    def validate_password_strength(password)
      @validator.validate(password)
    end
    
    private
    
    def default_options
      {
        use_crypto_random: true,
        word_list_size: 7776,  # Standard DiceWare word list size
        min_word_count: 4,
        max_word_count: 20,
        entropy_threshold: 128  # bits
      }
    end
    
    def validate_inputs(word_count, separator)
      raise ArgumentError, "Word count must be between #{@options[:min_word_count]} and #{@options[:max_word_count]}" unless 
        word_count.between?(@options[:min_word_count], @options[:max_word_count])
      
      raise ArgumentError, "Separator cannot be empty" if separator.nil? || separator.empty?
    end
    
    def validate_password_length(length)
      raise ArgumentError, "Password length must be at least 8 characters" if length < 8
      raise ArgumentError, "Password length cannot exceed 256 characters" if length > 256
    end
    
    def generate_words(count)
      words = []
      
      count.times do
        dice_rolls = generate_dice_rolls
        word = @word_list.get_word(dice_rolls)
        words << word
      end
      
      words
    end
    
    def generate_dice_rolls
      rolls = []
      
      5.times do
        if @options[:use_crypto_random]
          rolls << SecureRandom.random_number(6) + 1
        else
          rolls << rand(1..6)
        end
      end
      
      rolls.join
    end
    
    def generate_numbers(count)
      numbers = []
      
      count.times do
        if @options[:use_crypto_random]
          numbers << SecureRandom.random_number(1000)
        else
          numbers << rand(1000)
        end
      end
      
      numbers
    end
    
    def generate_symbols(count)
      symbols = ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '=', '+']
      generated_symbols = []
      
      count.times do
        if @options[:use_crypto_random]
          generated_symbols << symbols[SecureRandom.random_number(symbols.length)]
        else
          generated_symbols << symbols.sample
        end
      end
      
      generated_symbols
    end
    
    def generate_random_string(length, character_set)
      password = ''
      
      length.times do
        if @options[:use_crypto_random]
          password += character_set[SecureRandom.random_number(character_set.length)]
        else
          password += character_set.sample
        end
      end
      
      password
    end
    
    def get_character_set(type)
      case type
      when :lowercase
        'abcdefghijklmnopqrstuvwxyz'
      when :uppercase
        'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
      when :numbers
        '0123456789'
      when :symbols
        '!@#$%^&*()_+-=[]{}|;:,.<>?'
      when :alphanumeric
        'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
      when :all
        'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?'
      else
        raise ArgumentError, "Unknown character set: #{type}"
      end
    end
    
    def calculate_entropy(word_count, include_numbers = false, include_symbols = false)
      @entropy_calculator.calculate(word_count, include_numbers, include_symbols)
    end
    
    def calculate_entropy_for_chars(length, character_set_size)
      @entropy_calculator.calculate_for_chars(length, character_set_size)
    end
    
    def assess_strength(password)
      @validator.assess_strength(password)
    end
  end
end

Word List Management

# lib/diceware/word_list.rb
require 'csv'
require 'yaml'

module DiceWare
  class WordList
    attr_reader :words, :size
    
    def initialize(file_path = nil)
      @words = {}
      @file_path = file_path || default_word_list_path
      load_word_list
    end
    
    def get_word(dice_rolls)
      raise ArgumentError, "Invalid dice rolls: #{dice_rolls}" unless valid_dice_rolls?(dice_rolls)
      
      word = @words[dice_rolls]
      raise "Word not found for dice rolls: #{dice_rolls}" unless word
      
      word
    end
    
    def get_word_by_index(index)
      raise ArgumentError, "Index out of range" unless index.between?(0, @size - 1)
      
      dice_rolls = index_to_dice_rolls(index)
      get_word(dice_rolls)
    end
    
    def search_words(pattern)
      @words.select { |rolls, word| word.match?(/#{pattern}/i) }
    end
    
    def get_random_word
      random_rolls = generate_random_rolls
      get_word(random_rolls)
    end
    
    def add_custom_word(dice_rolls, word)
      raise ArgumentError, "Invalid dice rolls: #{dice_rolls}" unless valid_dice_rolls?(dice_rolls)
      raise ArgumentError, "Word cannot be empty" if word.nil? || word.empty?
      
      @words[dice_rolls] = word.strip
    end
    
    def remove_word(dice_rolls)
      @words.delete(dice_rolls)
    end
    
    def save_custom_word_list(file_path)
      File.open(file_path, 'w') do |file|
        @words.each do |rolls, word|
          file.puts "#{rolls}\t#{word}"
        end
      end
    end
    
    def load_custom_word_list(file_path)
      raise "File not found: #{file_path}" unless File.exist?(file_path)
      
      File.readlines(file_path).each do |line|
        next if line.strip.empty? || line.start_with?('#')
        
        parts = line.strip.split("\t")
        next unless parts.length >= 2
        
        dice_rolls = parts[0]
        word = parts[1]
        
        add_custom_word(dice_rolls, word)
      end
    end
    
    def statistics
      {
        total_words: @words.size,
        average_word_length: calculate_average_length,
        longest_word: find_longest_word,
        shortest_word: find_shortest_word,
        unique_characters: calculate_unique_characters
      }
    end
    
    private
    
    def default_word_list_path
      File.join(File.dirname(__FILE__), '..', '..', 'data', 'wordlists', 'diceware.txt')
    end
    
    def load_word_list
      raise "Word list file not found: #{@file_path}" unless File.exist?(@file_path)
      
      File.readlines(@file_path).each_with_index do |line, index|
        next if line.strip.empty? || line.start_with?('#')
        
        parts = line.strip.split("\t")
        next unless parts.length >= 2
        
        dice_rolls = parts[0]
        word = parts[1]
        
        @words[dice_rolls] = word
      end
      
      @size = @words.size
    end
    
    def valid_dice_rolls?(rolls)
      rolls.is_a?(String) && rolls.length == 5 && rolls.match?(/^[1-6]{5}$/)
    end
    
    def index_to_dice_rolls(index)
      # Convert index to 5-digit dice rolls
      rolls = []
      temp_index = index
      
      5.times do
        rolls.unshift((temp_index % 6) + 1)
        temp_index /= 6
      end
      
      rolls.join
    end
    
    def generate_random_rolls
      rolls = []
      5.times { rolls << rand(1..6) }
      rolls.join
    end
    
    def calculate_average_length
      return 0 if @words.empty?
      
      total_length = @words.values.sum(&:length)
      total_length.to_f / @words.size
    end
    
    def find_longest_word
      @words.values.max_by(&:length)
    end
    
    def find_shortest_word
      @words.values.min_by(&:length)
    end
    
    def calculate_unique_characters
      all_chars = @words.values.join.downcase.chars.uniq
      all_chars.size
    end
  end
end

Entropy Calculation

# lib/diceware/entropy.rb
require 'math'

module DiceWare
  class Entropy
    def calculate(word_count, include_numbers = false, include_symbols = false)
      # Base entropy from DiceWare word list (log2(7776) ≈ 12.9 bits per word)
      base_entropy = word_count * Math.log2(7776)
      
      # Additional entropy from numbers (log2(1000) ≈ 9.97 bits per number)
      if include_numbers
        base_entropy += word_count * Math.log2(1000)
      end
      
      # Additional entropy from symbols (log2(14) ≈ 3.81 bits per symbol)
      if include_symbols
        base_entropy += word_count * Math.log2(14)
      end
      
      base_entropy.round(2)
    end
    
    def calculate_for_chars(length, character_set_size)
      length * Math.log2(character_set_size).round(2)
    end
    
    def calculate_for_password(password)
      # Calculate entropy based on character frequency
      char_frequency = calculate_character_frequency(password)
      entropy = 0
      
      char_frequency.each do |char, frequency|
        probability = frequency.to_f / password.length
        entropy -= probability * Math.log2(probability) if probability > 0
      end
      
      entropy.round(2)
    end
    
    def estimate_cracking_time(entropy_bits, attempts_per_second = 1_000_000_000)
      # Estimate time to crack password with given entropy
      total_combinations = 2 ** entropy_bits
      seconds_to_crack = total_combinations / (2 * attempts_per_second) # Divide by 2 for average case
      
      format_time(seconds_to_crack)
    end
    
    def entropy_to_strength_level(entropy_bits)
      case entropy_bits
      when 0..30
        :very_weak
      when 31..50
        :weak
      when 51..80
        :moderate
      when 81..120
        :strong
      when 121..160
        :very_strong
      else
        :extremely_strong
      end
    end
    
    private
    
    def calculate_character_frequency(password)
      frequency = Hash.new(0)
      password.each_char { |char| frequency[char] += 1 }
      frequency
    end
    
    def format_time(seconds)
      if seconds < 60
        "#{seconds.round(2)} seconds"
      elsif seconds < 3600
        "#{(seconds / 60).round(2)} minutes"
      elsif seconds < 86400
        "#{(seconds / 3600).round(2)} hours"
      elsif seconds < 31536000
        "#{(seconds / 86400).round(2)} days"
      else
        "#{(seconds / 31536000).round(2)} years"
      end
    end
  end
end

Password Validation

# lib/diceware/validator.rb
require 'securerandom'

module DiceWare
  class Validator
    def initialize
      @common_passwords = load_common_passwords
      @patterns = load_patterns
    end
    
    def validate(password)
      results = {
        valid: true,
        errors: [],
        warnings: [],
        suggestions: []
      }
      
      # Basic validation
      validate_length(password, results)
      validate_complexity(password, results)
      validate_common_passwords(password, results)
      validate_patterns(password, results)
      validate_entropy(password, results)
      
      results[:valid] = results[:errors].empty?
      results
    end
    
    def assess_strength(password)
      score = 0
      feedback = []
      
      # Length scoring
      if password.length >= 12
        score += 25
      elsif password.length >= 8
        score += 15
      else
        score += 5
        feedback << "Consider using a longer password"
      end
      
      # Character variety scoring
      score += assess_character_variety(password, feedback)
      
      # Entropy scoring
      entropy_score = assess_entropy(password, feedback)
      score += entropy_score
      
      # Pattern scoring
      score += assess_patterns(password, feedback)
      
      # Final assessment
      strength_level = determine_strength_level(score)
      
      {
        score: [score, 100].min,
        level: strength_level,
        feedback: feedback,
        entropy: calculate_password_entropy(password)
      }
    end
    
    def check_password_policy(password, policy = default_policy)
      violations = []
      
      policy.each do |rule, value|
        case rule
        when :min_length
          violations << "Password must be at least #{value} characters long" if password.length < value
        when :max_length
          violations << "Password must be no more than #{value} characters long" if password.length > value
        when :require_uppercase
          violations << "Password must contain uppercase letters" unless password.match?(/[A-Z]/)
        when :require_lowercase
          violations << "Password must contain lowercase letters" unless password.match?(/[a-z]/)
        when :require_numbers
          violations << "Password must contain numbers" unless password.match?(/\d/)
        when :require_symbols
          violations << "Password must contain symbols" unless password.match?(/[^a-zA-Z0-9]/)
        when :forbid_common_words
          violations << "Password contains common words" if contains_common_words?(password)
        when :forbid_user_info
          violations << "Password contains user information" if contains_user_info?(password)
        end
      end
      
      {
        compliant: violations.empty?,
        violations: violations
      }
    end
    
    private
    
    def default_policy
      {
        min_length: 8,
        max_length: 128,
        require_uppercase: true,
        require_lowercase: true,
        require_numbers: true,
        require_symbols: false,
        forbid_common_words: true,
        forbid_user_info: true
      }
    end
    
    def validate_length(password, results)
      if password.length < 8
        results[:errors] << "Password must be at least 8 characters long"
      elsif password.length > 128
        results[:errors] << "Password cannot exceed 128 characters"
      end
      
      if password.length < 12
        results[:warnings] << "Consider using a password with at least 12 characters"
      end
    end
    
    def validate_complexity(password, results)
      has_uppercase = password.match?(/[A-Z]/)
      has_lowercase = password.match?(/[a-z]/)
      has_numbers = password.match?(/\d/)
      has_symbols = password.match?(/[^a-zA-Z0-9]/)
      
      complexity_score = [has_uppercase, has_lowercase, has_numbers, has_symbols].count(true)
      
      if complexity_score < 3
        results[:warnings] << "Password should contain at least 3 different character types"
      end
      
      unless has_uppercase
        results[:suggestions] << "Add uppercase letters"
      end
      
      unless has_lowercase
        results[:suggestions] << "Add lowercase letters"
      end
      
      unless has_numbers
        results[:suggestions] << "Add numbers"
      end
      
      unless has_symbols
        results[:suggestions] << "Add symbols"
      end
    end
    
    def validate_common_passwords(password, results)
      if @common_passwords.include?(password.downcase)
        results[:errors] << "Password is too common"
      end
    end
    
    def validate_patterns(password, results)
      @patterns.each do |pattern, message|
        if password.match?(pattern)
          results[:warnings] << message
        end
      end
    end
    
    def validate_entropy(password, results)
      entropy = calculate_password_entropy(password)
      
      if entropy < 30
        results[:errors] << "Password has insufficient entropy"
      elsif entropy < 50
        results[:warnings] << "Password has low entropy"
      end
    end
    
    def assess_character_variety(password, feedback)
      score = 0
      
      if password.match?(/[A-Z]/)
        score += 10
      else
        feedback << "Add uppercase letters"
      end
      
      if password.match?(/[a-z]/)
        score += 10
      else
        feedback << "Add lowercase letters"
      end
      
      if password.match?(/\d/)
        score += 10
      else
        feedback << "Add numbers"
      end
      
      if password.match?(/[^a-zA-Z0-9]/)
        score += 15
      else
        feedback << "Add symbols"
      end
      
      score
    end
    
    def assess_entropy(password, feedback)
      entropy = calculate_password_entropy(password)
      
      case entropy
      when 0..30
        feedback << "Very low entropy"
        0
      when 31..50
        feedback << "Low entropy"
        10
      when 51..80
        feedback << "Moderate entropy"
        20
      when 81..120
        feedback << "Good entropy"
        30
      else
        feedback << "Excellent entropy"
        40
      end
    end
    
    def assess_patterns(password, feedback)
      score = 0
      
      # Check for repeated characters
      if password.match?(/(.)\1{2,}/)
        score -= 10
        feedback << "Avoid repeated characters"
      end
      
      # Check for sequential patterns
      if password.match?(/(abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz)/i)
        score -= 5
        feedback << "Avoid sequential patterns"
      end
      
      # Check for keyboard patterns
      if password.match?(/(qwerty|asdfgh|zxcvbn|123456)/i)
        score -= 15
        feedback << "Avoid keyboard patterns"
      end
      
      score
    end
    
    def determine_strength_level(score)
      case score
      when 0..30
        :very_weak
      when 31..50
        :weak
      when 51..70
        :moderate
      when 71..85
        :strong
      when 86..95
        :very_strong
      else
        :extremely_strong
      end
    end
    
    def calculate_password_entropy(password)
      char_frequency = Hash.new(0)
      password.each_char { |char| char_frequency[char] += 1 }
      
      entropy = 0
      char_frequency.each do |char, frequency|
        probability = frequency.to_f / password.length
        entropy -= probability * Math.log2(probability) if probability > 0
      end
      
      entropy.round(2)
    end
    
    def contains_common_words?(password)
      common_words = %w[password passwd admin root user login welcome hello world]
      password.downcase.split(/[^a-zA-Z]/).any? { |word| common_words.include?(word) }
    end
    
    def contains_user_info?(password)
      # This would typically check against user profile information
      # For now, we'll check for common patterns
      false
    end
    
    def load_common_passwords
      # Load from file or use built-in list
      common_passwords = %w[
        password 123456 123456789 qwerty abc123 password123
        admin root user guest welcome hello world
      ]
      
      # Load additional common passwords from file if available
      common_file = File.join(File.dirname(__FILE__), '..', '..', 'data', 'common_passwords.txt')
      if File.exist?(common_file)
        common_passwords += File.readlines(common_file).map(&:strip)
      end
      
      common_passwords.map(&:downcase).to_set
    end
    
    def load_patterns
      {
        /^[a-z]+$/i => "Consider mixing character types",
        /^[A-Z]+$/i => "Consider mixing character types",
        /^\d+$/ => "Consider mixing character types",
        /(.)\1{3,}/ => "Avoid repeated characters",
        /(abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz)/i => "Avoid sequential patterns"
      }
    end
  end
end

OpenLDAP Integration

# lib/integration/ldap_client.rb
require 'net/ldap'
require 'digest'

module DiceWare
  class LDAPClient
    def initialize(config)
      @config = config
      @ldap = Net::LDAP.new(
        host: @config[:host],
        port: @config[:port],
        auth: {
          method: :simple,
          username: @config[:bind_dn],
          password: @config[:bind_password]
        }
      )
    end
    
    def authenticate_user(username, password)
      user_dn = find_user_dn(username)
      return false unless user_dn
      
      # Test authentication
      test_ldap = Net::LDAP.new(
        host: @config[:host],
        port: @config[:port],
        auth: {
          method: :simple,
          username: user_dn,
          password: password
        }
      )
      
      test_ldap.bind
    end
    
    def change_password(username, new_password, old_password = nil)
      user_dn = find_user_dn(username)
      return false unless user_dn
      
      # Verify old password if provided
      if old_password && !authenticate_user(username, old_password)
        return false
      end
      
      # Hash the new password
      hashed_password = hash_password(new_password)
      
      # Update password
      @ldap.replace_attribute(user_dn, :userPassword, hashed_password)
    end
    
    def set_password(username, new_password)
      user_dn = find_user_dn(username)
      return false unless user_dn
      
      hashed_password = hash_password(new_password)
      
      @ldap.replace_attribute(user_dn, :userPassword, hashed_password)
    end
    
    def create_user(user_info)
      user_dn = "uid=#{user_info[:username]},#{@config[:user_base]}"
      
      attributes = {
        objectClass: ['inetOrgPerson', 'organizationalPerson', 'person', 'top'],
        uid: user_info[:username],
        cn: user_info[:full_name],
        sn: user_info[:last_name],
        givenName: user_info[:first_name],
        mail: user_info[:email],
        userPassword: hash_password(user_info[:password])
      }
      
      @ldap.add(dn: user_dn, attributes: attributes)
    end
    
    def delete_user(username)
      user_dn = find_user_dn(username)
      return false unless user_dn
      
      @ldap.delete(dn: user_dn)
    end
    
    def list_users
      users = []
      
      @ldap.search(
        base: @config[:user_base],
        filter: Net::LDAP::Filter.eq('objectClass', 'inetOrgPerson'),
        attributes: ['uid', 'cn', 'mail']
      ) do |entry|
        users << {
          username: entry[:uid].first,
          full_name: entry[:cn].first,
          email: entry[:mail].first
        }
      end
      
      users
    end
    
    def find_user_dn(username)
      @ldap.search(
        base: @config[:user_base],
        filter: Net::LDAP::Filter.eq('uid', username),
        attributes: ['dn']
      ).first&.dn
    end
    
    private
    
    def hash_password(password)
      # Use SSHA (Salted SHA) for LDAP
      salt = SecureRandom.random_bytes(4)
      digest = Digest::SHA1.digest(password + salt)
      "{SSHA}" + Base64.strict_encode64(digest + salt)
    end
  end
end

Command Line Interface

#!/usr/bin/env ruby
# bin/diceware

require 'optparse'
require 'json'
require_relative '../lib/diceware/generator'

class DiceWareCLI
  def initialize
    @options = {}
    @generator = DiceWare::Generator.new
  end
  
  def run
    parse_options
    
    case @options[:command]
    when 'generate'
      generate_passwords
    when 'validate'
      validate_password
    when 'batch'
      batch_generate
    when 'strength'
      check_strength
    else
      show_help
    end
  end
  
  private
  
  def parse_options
    OptionParser.new do |opts|
      opts.banner = "Usage: diceware [command] [options]"
      
      opts.on('-w', '--words COUNT', Integer, 'Number of words to generate') do |count|
        @options[:word_count] = count
      end
      
      opts.on('-s', '--separator SEP', 'Separator between words') do |sep|
        @options[:separator] = sep
      end
      
      opts.on('-n', '--numbers', 'Include numbers') do
        @options[:include_numbers] = true
      end
      
      opts.on('-y', '--symbols', 'Include symbols') do
        @options[:include_symbols] = true
      end
      
      opts.on('-c', '--count COUNT', Integer, 'Number of passwords to generate') do |count|
        @options[:count] = count
      end
      
      opts.on('-p', '--password PASSWORD', 'Password to validate') do |password|
        @options[:password] = password
      end
      
      opts.on('-o', '--output FORMAT', 'Output format (text, json, csv)') do |format|
        @options[:output_format] = format
      end
      
      opts.on('-f', '--file FILE', 'Output to file') do |file|
        @options[:output_file] = file
      end
      
      opts.on('-v', '--verbose', 'Verbose output') do
        @options[:verbose] = true
      end
      
      opts.on('-h', '--help', 'Show help') do
        @options[:command] = 'help'
      end
    end.parse!
    
    # Set defaults
    @options[:word_count] ||= 6
    @options[:separator] ||= ' '
    @options[:count] ||= 1
    @options[:output_format] ||= 'text'
    @options[:command] ||= 'generate'
  end
  
  def generate_passwords
    passwords = []
    
    @options[:count].times do
      if @options[:include_numbers] || @options[:include_symbols]
        result = @generator.generate_passphrase(
          @options[:word_count],
          @options[:separator],
          @options[:include_numbers],
          @options[:include_symbols]
        )
      else
        result = @generator.generate_password(@options[:word_count], @options[:separator])
      end
      
      passwords << result
    end
    
    output_passwords(passwords)
  end
  
  def validate_password
    unless @options[:password]
      puts "Error: Password required for validation"
      exit 1
    end
    
    result = @generator.validate_password_strength(@options[:password])
    
    case @options[:output_format]
    when 'json'
      puts JSON.pretty_generate(result)
    else
      display_validation_result(result)
    end
  end
  
  def batch_generate
    passwords = @generator.batch_generate(@options[:count], @options[:word_count])
    output_passwords(passwords)
  end
  
  def check_strength
    unless @options[:password]
      puts "Error: Password required for strength check"
      exit 1
    end
    
    result = @generator.validate_password_strength(@options[:password])
    
    case @options[:output_format]
    when 'json'
      puts JSON.pretty_generate(result)
    else
      display_strength_result(result)
    end
  end
  
  def output_passwords(passwords)
    case @options[:output_format]
    when 'json'
      output = JSON.pretty_generate(passwords)
    when 'csv'
      output = generate_csv(passwords)
    else
      output = generate_text(passwords)
    end
    
    if @options[:output_file]
      File.write(@options[:output_file], output)
      puts "Passwords saved to #{@options[:output_file]}"
    else
      puts output
    end
  end
  
  def generate_text(passwords)
    output = []
    
    passwords.each_with_index do |result, index|
      output << "Password #{index + 1}:"
      output << "  Password: #{result[:password]}"
      
      if @options[:verbose]
        output << "  Words: #{result[:words].join(', ')}"
        output << "  Entropy: #{result[:entropy]} bits"
        output << "  Strength: #{result[:strength][:level]}"
        output << "  Generated: #{result[:generated_at]}"
      end
      
      output << ""
    end
    
    output.join("\n")
  end
  
  def generate_csv(passwords)
    require 'csv'
    
    CSV.generate do |csv|
      csv << ['Password', 'Entropy', 'Strength', 'Generated']
      
      passwords.each do |result|
        csv << [
          result[:password],
          result[:entropy],
          result[:strength][:level],
          result[:generated_at]
        ]
      end
    end
  end
  
  def display_validation_result(result)
    puts "Password Validation Result:"
    puts "Valid: #{result[:valid]}"
    
    unless result[:errors].empty?
      puts "Errors:"
      result[:errors].each { |error| puts "  - #{error}" }
    end
    
    unless result[:warnings].empty?
      puts "Warnings:"
      result[:warnings].each { |warning| puts "  - #{warning}" }
    end
    
    unless result[:suggestions].empty?
      puts "Suggestions:"
      result[:suggestions].each { |suggestion| puts "  - #{suggestion}" }
    end
  end
  
  def display_strength_result(result)
    puts "Password Strength Analysis:"
    puts "Score: #{result[:score]}/100"
    puts "Level: #{result[:level]}"
    puts "Entropy: #{result[:entropy]} bits"
    
    unless result[:feedback].empty?
      puts "Feedback:"
      result[:feedback].each { |feedback| puts "  - #{feedback}" }
    end
  end
  
  def show_help
    puts <<~HELP
      DiceWare Password Generator
      
      Commands:
        generate    Generate DiceWare passwords
        validate    Validate password strength
        batch       Generate multiple passwords
        strength    Check password strength
      
      Options:
        -w, --words COUNT      Number of words (default: 6)
        -s, --separator SEP    Separator between words (default: ' ')
        -n, --numbers          Include numbers
        -y, --symbols          Include symbols
        -c, --count COUNT      Number of passwords to generate
        -p, --password PASS    Password to validate/check
        -o, --output FORMAT    Output format (text, json, csv)
        -f, --file FILE        Output to file
        -v, --verbose          Verbose output
        -h, --help             Show this help
      
      Examples:
        diceware generate -w 6
        diceware generate -w 8 -n -y -c 5
        diceware validate -p "mypassword123"
        diceware strength -p "correct horse battery staple"
    HELP
  end
end

# Run CLI if called directly
if __FILE__ == $0
  DiceWareCLI.new.run
end

Testing Framework

Unit Tests

# tests/unit/test_generator.rb
require 'test/unit'
require_relative '../../lib/diceware/generator'

class TestGenerator < Test::Unit::TestCase
  def setup
    @generator = DiceWare::Generator.new
  end
  
  def test_generate_password
    result = @generator.generate_password(6)
    
    assert_not_nil result[:password]
    assert_equal 6, result[:words].length
    assert result[:entropy] > 0
    assert_not_nil result[:strength]
  end
  
  def test_generate_passphrase_with_numbers
    result = @generator.generate_passphrase(6, ' ', true, false)
    
    assert_not_nil result[:password]
    assert result[:password].match?(/\d/)
    assert result[:entropy] > 0
  end
  
  def test_generate_passphrase_with_symbols
    result = @generator.generate_passphrase(6, ' ', false, true)
    
    assert_not_nil result[:password]
    assert result[:password].match?(/[^a-zA-Z0-9\s]/)
    assert result[:entropy] > 0
  end
  
  def test_batch_generate
    passwords = @generator.batch_generate(5, 6)
    
    assert_equal 5, passwords.length
    passwords.each do |result|
      assert_not_nil result[:password]
      assert_equal 6, result[:words].length
    end
  end
  
  def test_validate_password_strength
    result = @generator.validate_password_strength("password123")
    
    assert_not_nil result
    assert result.key?(:valid)
    assert result.key?(:errors)
    assert result.key?(:warnings)
  end
  
  def test_invalid_word_count
    assert_raise(ArgumentError) do
      @generator.generate_password(3)  # Too few words
    end
    
    assert_raise(ArgumentError) do
      @generator.generate_password(25)  # Too many words
    end
  end
end

Lessons Learned

Cryptography and Security

  • Entropy: Understanding and calculating password entropy
  • Random Number Generation: Secure random number generation techniques
  • Password Policies: Implementing comprehensive password validation
  • Security Best Practices: Following cryptographic security principles

Ruby Development

  • Object-Oriented Design: Clean class hierarchy and separation of concerns
  • Error Handling: Robust error handling and validation
  • Testing: Comprehensive unit testing for security-critical code
  • CLI Development: User-friendly command-line interface design

System Integration

  • LDAP Integration: OpenLDAP authentication and user management
  • Configuration Management: Flexible configuration system
  • Logging: Comprehensive logging for security auditing
  • Documentation: Clear documentation for security tools

Future Enhancements

Advanced Features

  • Hardware Security Modules: Integration with HSM for key generation
  • Multi-Factor Authentication: Integration with MFA systems
  • Password Managers: Integration with password manager applications
  • Biometric Integration: Support for biometric authentication

Security Improvements

  • Advanced Entropy: More sophisticated entropy calculations
  • Threat Modeling: Integration with threat modeling tools
  • Compliance: Automated compliance checking for password policies
  • Audit Logging: Enhanced audit logging and monitoring

Conclusion

The DiceWare Password Generator demonstrates comprehensive cryptographic security practices and Ruby development skills. Key achievements include:

  • Cryptographic Security: Implementation of DiceWare method for secure password generation
  • Entropy Calculation: Advanced entropy calculation and password strength assessment
  • System Integration: OpenLDAP integration for enterprise authentication
  • User Interface: Comprehensive CLI and validation tools
  • Testing: Thorough testing of security-critical components
  • Documentation: Clear documentation and usage examples

The project is available on GitHub and serves as a comprehensive example of cryptographic security implementation and password management systems.


This project represents my deep dive into cryptographic security and demonstrates how secure password generation can be implemented using established methods like DiceWare. The lessons learned here continue to influence my approach to security-critical applications and authentication systems.