diff --git a/README.md b/README.md index 7c173e8..6c84110 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,21 @@ Or install it yourself as: $ gem install zxcvbn + ## Configuration + +## Configuration + +Zxcvbn allows you to load custom dictionaries to enhance the password strength estimation. + +To load dictionaries, use the following syntax: + +```ruby +Zxcvbn.configure do |config| + config.add_dictionary('path/to/custom_dictionary.txt') + # Add more dictionaries as needed +end +``` + ## Usage ```ruby diff --git a/lib/zxcvbn.rb b/lib/zxcvbn.rb index f3ce0b4..3141ae5 100644 --- a/lib/zxcvbn.rb +++ b/lib/zxcvbn.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative "zxcvbn/config" require_relative "zxcvbn/adjacency_graphs" require_relative "zxcvbn/frequency_lists" require_relative "zxcvbn/matching" diff --git a/lib/zxcvbn/config.rb b/lib/zxcvbn/config.rb new file mode 100644 index 0000000..e5327e1 --- /dev/null +++ b/lib/zxcvbn/config.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Zxcvbn + module Config + def add_dictionary(path) + lines = File.read(path, encoding: "UTF-8") + .each_line(chomp: true) + .map(&:strip) + .reject(&:empty?) + name = File.basename(path, ".*") + ranked = Zxcvbn::Matching.new.build_ranked_dict(lines) + Zxcvbn::Matching.register_dictionary(name, ranked) + end + + module_function :add_dictionary + end + + class << self + def configure + block_given? ? yield(Config) : Config + end + + def config + Config + end + end +end diff --git a/lib/zxcvbn/matching.rb b/lib/zxcvbn/matching.rb index e9618b2..e00ef4e 100644 --- a/lib/zxcvbn/matching.rb +++ b/lib/zxcvbn/matching.rb @@ -2,6 +2,16 @@ module Zxcvbn class Matching + @custom_dictionaries = {}.freeze + + class << self + attr_reader :custom_dictionaries + + def register_dictionary(name, ranked) + @custom_dictionaries = @custom_dictionaries.merge(name => ranked).freeze + end + end + def build_ranked_dict(ordered_list) result = {} # rank starts at 1, not 0 @@ -14,7 +24,7 @@ def build_ranked_dict(ordered_list) def ranked_dictionaries @ranked_dictionaries ||= Zxcvbn.frequency_lists.transform_values do |lst| build_ranked_dict(lst) - end + end.merge! self.class.custom_dictionaries end def ranked_dictionaries_max_word_size diff --git a/spec/config_spec.rb b/spec/config_spec.rb new file mode 100644 index 0000000..495aef8 --- /dev/null +++ b/spec/config_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.describe "Zxcvbn::Config" do + describe "#add_dictionary" do + let(:dictionary_path) { "custom_dictionary.txt" } + let(:dictionary_contents) { "password_1\npassword_2\npassword_3" } + + before do + allow(File).to receive(:read).and_call_original + allow(File).to receive(:read) + .with(dictionary_path, hash_including(encoding: "UTF-8")) + .and_return(dictionary_contents) + end + + it "adds a custom dictionary to ranked_dictionaries" do + Zxcvbn.config.add_dictionary(dictionary_path) + + expected_dictionary = { + "custom_dictionary" => { + "password_1" => 1, + "password_2" => 2, + "password_3" => 3 + } + } + + expect(Zxcvbn::Matching.new.ranked_dictionaries).to include(expected_dictionary) + end + end + + describe ".configure" do + context "when called with a block" do + it "yields the Config module" do + expect { |b| Zxcvbn.configure(&b) }.to yield_with_args(Zxcvbn::Config) + end + end + + context "when called without a block" do + it "returns the Config module" do + expect(Zxcvbn.configure).to eq(Zxcvbn.config) + end + end + end + + describe ".config" do + it "returns the Config module" do + expect(Zxcvbn.config).to eq(Zxcvbn.config) + end + end +end