├── spec ├── fixtures │ ├── AD871.txt │ ├── A0F41.txt │ ├── 5BAA6.txt │ ├── 37D5B.txt │ └── 613D1.txt ├── pwned_spec.rb ├── support │ └── stub_pwned_range.rb ├── spec_helper.rb └── pwned │ ├── hashed_password_spec.rb │ ├── not_pwned_validator_spec.rb │ └── password_spec.rb ├── .yardopts ├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── .rspec ├── lib ├── pwned │ ├── version.rb │ ├── deep_merge.rb │ ├── error.rb │ ├── hashed_password.rb │ ├── password.rb │ ├── password_base.rb │ └── not_pwned_validator.rb ├── locale │ ├── en.yml │ ├── nl.yml │ └── fr.yml └── pwned.rb ├── bin ├── setup ├── console └── pwned ├── .gitignore ├── Rakefile ├── Gemfile ├── LICENSE.txt ├── pwned.gemspec ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md └── README.md /spec/fixtures/AD871.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --output-dir docs -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: philnash -------------------------------------------------------------------------------- /spec/fixtures/A0F41.txt: -------------------------------------------------------------------------------- 1 | 7D58E726EB382EF3AE22EE95D07C0C4B40E:1 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/pwned/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Pwned 4 | ## 5 | # The current version of the +pwned+ gem. 6 | VERSION = '2.4.1' 7 | end 8 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | 13 | Gemfile.lock -------------------------------------------------------------------------------- /lib/locale/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | errors: 3 | messages: 4 | not_pwned: has previously appeared in a data breach and should not be used 5 | pwned_error: could not be verified against the past data breaches 6 | -------------------------------------------------------------------------------- /lib/locale/nl.yml: -------------------------------------------------------------------------------- 1 | nl: 2 | errors: 3 | messages: 4 | not_pwned: "is eerder verschenen in een datalek en mag niet worden gebruikt" 5 | pwned_error: "kon niet worden geverifieerd in eerdere datalekken" 6 | -------------------------------------------------------------------------------- /lib/locale/fr.yml: -------------------------------------------------------------------------------- 1 | fr: 2 | errors: 3 | messages: 4 | not_pwned: "est déjà apparu dans une brèche de données et ne doit pas être utilisé" 5 | pwned_error: "n'a pas pu être vérifié par rapport aux brèches de données passées" 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | require "yard" 4 | 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | task :default => :spec 8 | 9 | YARD::Rake::YardocTask.new do |t| 10 | t.options = ["--output-dir", "docs"] 11 | end -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in pwned.gemspec 6 | gemspec 7 | 8 | # Allows to switch Rails version in the build matrix 9 | gem "activemodel", ENV["RAILS_VERSION"] ? "~> #{ENV["RAILS_VERSION"]}" : nil 10 | -------------------------------------------------------------------------------- /lib/pwned/deep_merge.rb: -------------------------------------------------------------------------------- 1 | module DeepMerge 2 | refine Hash do 3 | def deep_merge(other) 4 | self.merge(other) do |key, this_val, other_val| 5 | if this_val.is_a?(Hash) && other_val.is_a?(Hash) 6 | this_val.deep_merge(other_val) 7 | else 8 | other_val 9 | end 10 | end 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /spec/pwned_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Pwned do 2 | it "has a version number" do 3 | expect(Pwned::VERSION).not_to be nil 4 | end 5 | 6 | describe "#hash_password" do 7 | it "returns an uppercase hash of the password" do 8 | expect(Pwned.hash_password("password")).to eq("5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8") 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "pwned" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /spec/support/stub_pwned_range.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.around :example, :pwned_range do |example| 3 | pwned_range = example.metadata[:pwned_range] 4 | File.open(File.expand_path("../fixtures/#{pwned_range}.txt", __dir__)) do |body| 5 | uri = "https://api.pwnedpasswords.com/range/#{pwned_range}" 6 | @stub = stub_request(:get, uri).to_return(body: body) 7 | example.run 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/pwned/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Pwned 4 | ## 5 | # A base error for HTTP request errors that may be thrown when making requests 6 | # to the Pwned Passwords API. 7 | # 8 | # @see Pwned::Password#pwned? 9 | # @see Pwned::Password#pwned_count 10 | class Error < StandardError 11 | end 12 | 13 | ## 14 | # An error to represent when the Pwned Passwords API times out. 15 | # 16 | # @see Pwned::Password#pwned? 17 | # @see Pwned::Password#pwned_count 18 | class TimeoutError < Error 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "webmock/rspec" 3 | require "pwned" 4 | 5 | # Easily stub pwned password hash range API requests 6 | require_relative "support/stub_pwned_range" 7 | 8 | # No network requests in specs 9 | WebMock.disable_net_connect! 10 | 11 | RSpec.configure do |config| 12 | # Enable flags like --only-failures and --next-failure 13 | config.example_status_persistence_file_path = ".rspec_status" 14 | 15 | # Disable RSpec exposing methods globally on `Module` and `main` 16 | config.disable_monkey_patching! 17 | 18 | config.expect_with :rspec do |c| 19 | c.syntax = :expect 20 | end 21 | 22 | config.after do |c| 23 | Pwned.default_request_options = {} 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Phil Nash 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /pwned.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("../lib", __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require "pwned/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "pwned" 7 | spec.version = Pwned::VERSION 8 | spec.authors = ["Phil Nash"] 9 | spec.email = ["philnash@gmail.com"] 10 | 11 | spec.summary = %q{Tools to use the Pwned Passwords API.} 12 | spec.description = %q{Tools to use the Pwned Passwords API.} 13 | spec.homepage = "https://github.com/philnash/pwned" 14 | spec.license = "MIT" 15 | 16 | spec.metadata = { 17 | "bug_tracker_uri" => "https://github.com/philnash/pwned/issues", 18 | "change_log_uri" => "https://github.com/philnash/pwned/blob/master/CHANGELOG.md", 19 | "documentation_uri" => "https://www.rubydoc.info/gems/pwned", 20 | "homepage_uri" => "https://github.com/philnash/pwned", 21 | "source_code_uri" => "https://github.com/philnash/pwned" 22 | } 23 | 24 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 25 | f.match(%r{^(test|spec|features)/}) 26 | end 27 | spec.require_paths = ["lib"] 28 | spec.executables = ["pwned"] 29 | 30 | spec.add_development_dependency "bundler", ">= 1.16", "< 3.0" 31 | spec.add_development_dependency "rake", "~> 13.0" 32 | spec.add_development_dependency "rspec", "~> 3.0" 33 | spec.add_development_dependency "webmock", "~> 3.3" 34 | spec.add_development_dependency "yard", "~> 0.9.12" 35 | end 36 | -------------------------------------------------------------------------------- /bin/pwned: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "pwned" 4 | require "optparse" 5 | require "io/console" 6 | 7 | options = {} 8 | parser = OptionParser.new do |opts| 9 | opts.banner = <<-USAGE 10 | Usage: pwned 11 | 12 | Tests a password against the Pwned Passwords API using the k-anonymity model, 13 | which avoids sending the entire password to the service.opts 14 | 15 | If the password has been found in a publicly available breach then this tool 16 | will report how many times it has been seen. Otherwise the tool will report that 17 | the password has not been found in a public breach yet. 18 | 19 | USAGE 20 | 21 | opts.version = Pwned::VERSION 22 | 23 | opts.on("-s", "--secret", "Enter password without displaying characters.\n#{" "* 37}Overrides provided arguments.") 24 | opts.on_tail("-h", "--help", "Show help.") 25 | opts.on_tail("-v", "--version", "Show version number.\n\n") 26 | end 27 | 28 | parser.parse!(ARGV, into: options) 29 | 30 | if options[:help] 31 | puts parser.help 32 | exit 33 | end 34 | if options[:version] 35 | puts parser.ver 36 | exit 37 | end 38 | password_to_test = ARGV.first 39 | if options[:secret] 40 | password_to_test = STDIN.getpass("Password: ") 41 | end 42 | if !password_to_test || password_to_test.strip == "" 43 | puts parser.help 44 | exit 45 | end 46 | password = Pwned::Password.new(password_to_test || ARGV.first) 47 | if password.pwned? 48 | puts "Pwned!\nThe password has been found in public breaches #{password.pwned_count} times." 49 | else 50 | puts "The password has not been found in a public breach." 51 | end 52 | 53 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | ruby: [2.6, 2.7, "3.0", 3.1, 3.2, 3.3, head] 12 | rails: [4.2.11.3, 5.0.7.2, 5.1.7, 5.2.4.4, 6.0.3.4, 6.1.0, 7.0.3.1] 13 | exclude: 14 | # Ruby 2.6 and Rails 7 do not get along together. 15 | - ruby: 2.6 16 | rails: 7.0.3.1 17 | # Ruby 3.0 and Rails 5 do not get along together. 18 | - ruby: "3.0" 19 | rails: 5.0.7.2 20 | - ruby: "3.0" 21 | rails: 5.1.7 22 | - ruby: "3.0" 23 | rails: 5.2.4.4 24 | - ruby: 3.1 25 | rails: 5.0.7.2 26 | - ruby: 3.1 27 | rails: 5.1.7 28 | - ruby: 3.1 29 | rails: 5.2.4.4 30 | - ruby: 3.2 31 | rails: 5.0.7.2 32 | - ruby: 3.2 33 | rails: 5.1.7 34 | - ruby: 3.2 35 | rails: 5.2.4.4 36 | - ruby: 3.3 37 | rails: 5.0.7.2 38 | - ruby: 3.3 39 | rails: 5.1.7 40 | - ruby: 3.3 41 | rails: 5.2.4.4 42 | - ruby: head 43 | rails: 5.0.7.2 44 | - ruby: head 45 | rails: 5.1.7 46 | - ruby: head 47 | rails: 5.2.4.4 48 | continue-on-error: ${{ endsWith(matrix.ruby, 'head') }} 49 | env: 50 | RAILS_VERSION: ${{ matrix.rails }} 51 | steps: 52 | - uses: actions/checkout@v4 53 | - name: Set up Ruby ${{ matrix.ruby }} 54 | uses: ruby/setup-ruby@v1 55 | with: 56 | ruby-version: ${{ matrix.ruby }} 57 | - name: "Install dependencies (rails: ${{matrix.rails}})" 58 | run: bundle install 59 | - name: Run tests 60 | run: bundle exec rspec 61 | -------------------------------------------------------------------------------- /spec/pwned/hashed_password_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Pwned::HashedPassword do 2 | let(:hashed_password) { Pwned::HashedPassword.new(password_hash) } 3 | let(:password) { "password" } 4 | let(:password_hash) { Pwned.hash_password(password) } 5 | 6 | it "initializes with a password" do 7 | expect(hashed_password.hashed_password).to eq("5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8") 8 | end 9 | 10 | describe "when given an integer" do 11 | let(:password_hash) { 123 } 12 | 13 | it "doesn't initialize" do 14 | expect { hashed_password }.to raise_error(TypeError) 15 | end 16 | end 17 | 18 | describe "when given an array" do 19 | let(:password_hash) { ["hello", "world"] } 20 | 21 | it "doesn't initialize" do 22 | expect { hashed_password }.to raise_error(TypeError) 23 | end 24 | end 25 | 26 | describe "when given a hash" do 27 | let(:password_hash) { { a: "b", c: "d" } } 28 | 29 | it "doesn't initialize" do 30 | expect { hashed_password }.to raise_error(TypeError) 31 | end 32 | end 33 | 34 | describe "when pwned", pwned_range: "5BAA6" do 35 | it "reports it is pwned" do 36 | expect(hashed_password.pwned?).to be true 37 | expect(@stub).to have_been_requested 38 | end 39 | 40 | it "reports it has been pwned many times" do 41 | expect(hashed_password.pwned_count).to eq(3303003) 42 | expect(@stub).to have_been_requested 43 | end 44 | 45 | describe "when given a lower case hash" do 46 | let(:hashed_password) { Pwned::HashedPassword.new(password_hash) } 47 | let(:password_hash) { Pwned.hash_password(password).downcase } 48 | 49 | it "upcases the hashed password" do 50 | expect(hashed_password.hashed_password).to eq("5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8") 51 | end 52 | 53 | it "reports it is pwned" do 54 | expect(hashed_password.pwned?).to be true 55 | expect(@stub).to have_been_requested 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/pwned/hashed_password.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "pwned/password_base" 4 | require "pwned/deep_merge" 5 | 6 | 7 | module Pwned 8 | ## 9 | # This class represents a hashed password. It does all the work of talking to the 10 | # Pwned Passwords API to find out if the password has been pwned. 11 | # @see https://haveibeenpwned.com/API/v2#PwnedPasswords 12 | class HashedPassword 13 | include PasswordBase 14 | using DeepMerge 15 | ## 16 | # Creates a new hashed password object. 17 | # 18 | # @example A simple password with the default request options 19 | # password = Pwned::HashedPassword.new("ABC123") 20 | # @example Setting the user agent and the read timeout of the request 21 | # password = Pwned::HashedPassword.new("ABC123", headers: { "User-Agent" => "My user agent" }, read_timout: 10) 22 | # 23 | # @param hashed_password [String] The hash of the password you want to check against the API. 24 | # @param [Hash] request_options Options that can be passed to +Net::HTTP.start+ when 25 | # calling the API. This overrides any keys specified in +Pwned.default_request_options+. 26 | # @option request_options [Symbol] :headers ({ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" }) 27 | # HTTP headers to include in the request 28 | # @option request_options [Symbol] :ignore_env_proxy (false) The library 29 | # will try to infer an HTTP proxy from the `http_proxy` environment 30 | # variable. If you do not want this behaviour, set this option to true. 31 | # @raise [TypeError] if the password is not a string. 32 | # @since 2.1.0 33 | def initialize(hashed_password, request_options={}) 34 | raise TypeError, "hashed_password must be of type String" unless hashed_password.is_a? String 35 | @hashed_password = hashed_password.upcase 36 | @request_options = Pwned.default_request_options.deep_merge(request_options) 37 | @request_headers = Hash(@request_options.delete(:headers)) 38 | @request_headers = DEFAULT_REQUEST_HEADERS.merge(@request_headers) 39 | @request_proxy = URI(@request_options.delete(:proxy)) if @request_options.key?(:proxy) 40 | @ignore_env_proxy = @request_options.delete(:ignore_env_proxy) || false 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/pwned/password.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "pwned/password_base" 4 | require "pwned/deep_merge" 5 | 6 | module Pwned 7 | ## 8 | # This class represents a password. It does all the work of talking to the 9 | # Pwned Passwords API to find out if the password has been pwned. 10 | # @see https://haveibeenpwned.com/API/v2#PwnedPasswords 11 | class Password 12 | include PasswordBase 13 | using DeepMerge 14 | ## 15 | # @return [String] the password that is being checked. 16 | # @since 1.0.0 17 | attr_reader :password 18 | 19 | ## 20 | # Creates a new password object. 21 | # 22 | # @example A simple password with the default request options 23 | # password = Pwned::Password.new("password") 24 | # @example Setting the user agent and the read timeout of the request 25 | # password = Pwned::Password.new("password", headers: { "User-Agent" => "My user agent" }, read_timout: 10) 26 | # 27 | # @param password [String] The password you want to check against the API. 28 | # @param [Hash] request_options Options that can be passed to +Net::HTTP.start+ when 29 | # calling the API. This overrides any keys specified in +Pwned.default_request_options+. 30 | # @option request_options [Symbol] :headers ({ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" }) 31 | # HTTP headers to include in the request 32 | # @option request_options [Symbol] :ignore_env_proxy (false) The library 33 | # will try to infer an HTTP proxy from the `http_proxy` environment 34 | # variable. If you do not want this behaviour, set this option to true. 35 | # @raise [TypeError] if the password is not a string. 36 | # @since 1.1.0 37 | def initialize(password, request_options={}) 38 | raise TypeError, "password must be of type String" unless password.is_a? String 39 | @password = password 40 | @hashed_password = Pwned.hash_password(password) 41 | @request_options = Pwned.default_request_options.deep_merge(request_options) 42 | @request_headers = Hash(@request_options.delete(:headers)) 43 | @request_headers = DEFAULT_REQUEST_HEADERS.merge(@request_headers) 44 | @request_proxy = URI(@request_options.delete(:proxy)) if @request_options.key?(:proxy) 45 | @ignore_env_proxy = @request_options.delete(:ignore_env_proxy) || false 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at philnash@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /lib/pwned.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "digest" 4 | require "pwned/version" 5 | require "pwned/error" 6 | require "pwned/password" 7 | require "pwned/hashed_password" 8 | 9 | begin 10 | # Load Rails and our custom validator 11 | require "active_model" 12 | require "pwned/not_pwned_validator" 13 | 14 | # Initialize I18n (validation error message) 15 | require "active_support/i18n" 16 | I18n.load_path.concat Dir[File.expand_path("locale/*.yml", __dir__)] 17 | rescue LoadError 18 | # Not a Rails project, no need to do anything 19 | end 20 | 21 | ## 22 | # The main namespace for +Pwned+. Includes convenience methods for getting the 23 | # results for a password. 24 | 25 | module Pwned 26 | @default_request_options = {} 27 | 28 | ## 29 | # The default request options passed to +Net::HTTP.start+ when calling the API. 30 | # 31 | # @return [Hash] 32 | # @see Pwned::Password#initialize 33 | def self.default_request_options 34 | @default_request_options 35 | end 36 | 37 | ## 38 | # Sets the default request options passed to +Net::HTTP.start+ when calling 39 | # the API. 40 | # 41 | # The default options may be overridden in +Pwned::Password#new+. 42 | # 43 | # @param [Hash] request_options 44 | # @see Pwned::Password#initialize 45 | def self.default_request_options=(request_options) 46 | @default_request_options = request_options 47 | end 48 | 49 | ## 50 | # Returns +true+ when the password has been pwned. 51 | # 52 | # @example 53 | # Pwned.pwned?("password") #=> true 54 | # Pwned.pwned?("pwned::password") #=> false 55 | # 56 | # @param password [String] The password you want to check against the API. 57 | # @param [Hash] request_options Options that can be passed to +Net::HTTP.start+ when 58 | # calling the API 59 | # @option request_options [Symbol] :headers ({ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" }) 60 | # HTTP headers to include in the request 61 | # @option request_options [Symbol] :ignore_env_proxy (false) The library 62 | # will try to infer an HTTP proxy from the `http_proxy` environment 63 | # variable. If you do not want this behaviour, set this option to true. 64 | # @return [Boolean] Whether the password appears in the data breaches or not. 65 | # @since 1.1.0 66 | def self.pwned?(password, request_options={}) 67 | Pwned::Password.new(password, request_options).pwned? 68 | end 69 | 70 | ## 71 | # Returns number of times the password has been pwned. 72 | # 73 | # @example 74 | # Pwned.pwned_count("password") #=> 3303003 75 | # Pwned.pwned_count("pwned::password") #=> 0 76 | # 77 | # @param password [String] The password you want to check against the API. 78 | # @param [Hash] request_options Options that can be passed to +Net::HTTP.start+ when 79 | # calling the API 80 | # @option request_options [Symbol] :headers ({ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" }) 81 | # HTTP headers to include in the request 82 | # @option request_options [Symbol] :ignore_env_proxy (false) The library 83 | # will try to infer an HTTP proxy from the `http_proxy` environment 84 | # variable. If you do not want this behaviour, set this option to true. 85 | # @return [Integer] The number of times the password has appeared in the data 86 | # breaches. 87 | # @since 1.1.0 88 | def self.pwned_count(password, request_options={}) 89 | Pwned::Password.new(password, request_options).pwned_count 90 | end 91 | 92 | ## 93 | # Returns the full SHA1 hash of the given password in uppercase. This can be safely passed around your code 94 | # before making the pwned request (e.g. dropped into a queue table). 95 | # 96 | # @example 97 | # Pwned.hash_password("password") #=> 5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8 98 | # 99 | # @param password [String] The password you want to check against the API 100 | # @return [String] An uppercase SHA1 hash of the password 101 | # @since 2.1.0 102 | def self.hash_password(password) 103 | Digest::SHA1.hexdigest(password).upcase 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/pwned/password_base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "digest" 4 | require "net/http" 5 | 6 | module Pwned 7 | ## 8 | # This class represents a password. It does all the work of talking to the 9 | # Pwned Passwords API to find out if the password has been pwned. 10 | # @see https://haveibeenpwned.com/API/v2#PwnedPasswords 11 | module PasswordBase 12 | ## 13 | # The base URL for the Pwned Passwords API 14 | API_URL = "https://api.pwnedpasswords.com/range/" 15 | 16 | ## 17 | # The number of characters from the start of the hash of the password that 18 | # are used to search for the range of passwords. 19 | HASH_PREFIX_LENGTH = 5 20 | 21 | ## 22 | # The total length of a SHA1 hash 23 | SHA1_LENGTH = 40 24 | 25 | ## 26 | # The default request headers that are used to make HTTP requests to the 27 | # API. A user agent is provided as requested in the documentation. 28 | # @see https://haveibeenpwned.com/API/v2#UserAgent 29 | DEFAULT_REQUEST_HEADERS = { 30 | "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" 31 | }.freeze 32 | 33 | ## 34 | # @example 35 | # password = Pwned::Password.new("password") 36 | # password.pwned? #=> true 37 | # password.pwned? #=> true 38 | # 39 | # @return [Boolean] +true+ when the password has been pwned. 40 | # @raise [Pwned::Error] if there are errors with the HTTP request. 41 | # @raise [Pwned::TimeoutError] if the HTTP request times out. 42 | # @since 1.0.0 43 | def pwned? 44 | pwned_count > 0 45 | end 46 | 47 | ## 48 | # @example 49 | # password = Pwned::Password.new("password") 50 | # password.pwned_count #=> 3303003 51 | # 52 | # @return [Integer] the number of times the password has been pwned. 53 | # @raise [Pwned::Error] if there are errors with the HTTP request. 54 | # @raise [Pwned::TimeoutError] if the HTTP request times out. 55 | # @since 1.0.0 56 | def pwned_count 57 | @pwned_count ||= fetch_pwned_count 58 | end 59 | 60 | ## 61 | # Returns the full SHA1 hash of the given password in uppercase. 62 | # @return [String] The full SHA1 hash of the given password. 63 | # @since 1.0.0 64 | attr_reader :hashed_password 65 | 66 | private 67 | 68 | attr_reader :request_options, :request_headers, :request_proxy, :ignore_env_proxy 69 | 70 | def fetch_pwned_count 71 | for_each_response_line do |line| 72 | next unless line.start_with?(hashed_password_suffix) 73 | # Count starts after the suffix, followed by a colon 74 | return line[(SHA1_LENGTH-HASH_PREFIX_LENGTH+1)..-1].to_i 75 | end 76 | 77 | # The hash was not found, we can assume the password is not pwned [yet] 78 | 0 79 | end 80 | 81 | def for_each_response_line(&block) 82 | begin 83 | with_http_response "#{API_URL}#{hashed_password_prefix}" do |response| 84 | response.value # raise if request was unsuccessful 85 | stream_response_lines(response, &block) 86 | end 87 | rescue Timeout::Error => e 88 | raise Pwned::TimeoutError, e.message 89 | rescue => e 90 | raise Pwned::Error, e.message 91 | end 92 | end 93 | 94 | def hashed_password_prefix 95 | @hashed_password[0...HASH_PREFIX_LENGTH] 96 | end 97 | 98 | def hashed_password_suffix 99 | @hashed_password[HASH_PREFIX_LENGTH..-1] 100 | end 101 | 102 | # Make a HTTP GET request given the URL and headers. 103 | # Yields a `Net::HTTPResponse`. 104 | def with_http_response(url, &block) 105 | uri = URI(url) 106 | 107 | request = Net::HTTP::Get.new(uri) 108 | request.initialize_http_header(request_headers) 109 | request_options[:use_ssl] = true 110 | 111 | environment_proxy = ignore_env_proxy ? nil : :ENV 112 | 113 | Net::HTTP.start( 114 | uri.host, 115 | uri.port, 116 | request_proxy&.host || environment_proxy, 117 | request_proxy&.port, 118 | request_proxy&.user, 119 | request_proxy&.password, 120 | request_options 121 | ) do |http| 122 | http.request(request, &block) 123 | end 124 | end 125 | 126 | # Stream a Net::HTTPResponse by line, handling lines that cross chunks. 127 | def stream_response_lines(response, &block) 128 | last_line = "" 129 | 130 | response.read_body do |chunk| 131 | chunk_lines = (last_line + chunk).lines 132 | # This could end with half a line, so save it for next time. If 133 | # chunk_lines is empty, pop returns nil, so this also ensures last_line 134 | # is always a string. 135 | last_line = chunk_lines.pop || "" 136 | chunk_lines.each(&block) 137 | end 138 | 139 | yield last_line unless last_line.empty? 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /lib/pwned/not_pwned_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ## 4 | # An +ActiveModel+ validator to check passwords against the Pwned Passwords API. 5 | # 6 | # @example Validate a password on a +User+ model with the default options. 7 | # class User < ApplicationRecord 8 | # validates :password, not_pwned: true 9 | # end 10 | # 11 | # @example Validate a password on a +User+ model with a custom error message. 12 | # class User < ApplicationRecord 13 | # validates :password, not_pwned: { message: "has been pwned %{count} times" } 14 | # end 15 | # 16 | # @example Validate a password on a +User+ model that allows the password to have been breached once. 17 | # class User < ApplicationRecord 18 | # validates :password, not_pwned: { threshold: 1 } 19 | # end 20 | # 21 | # @example Validate a password on a +User+ model, handling API errors in various ways 22 | # class User < ApplicationRecord 23 | # # The record is marked as invalid on network errors 24 | # # (error message "could not be verified against the past data breaches".) 25 | # validates :password, not_pwned: { on_error: :invalid } 26 | # 27 | # # The record is marked as invalid on network errors with custom error. 28 | # validates :password, not_pwned: { on_error: :invalid, error_message: "might be pwned" } 29 | # 30 | # # An error is raised on network errors. 31 | # # This means that `record.valid?` will raise `Pwned::Error`. 32 | # # Not recommended to use in production. 33 | # validates :password, not_pwned: { on_error: :raise_error } 34 | # 35 | # # Call custom proc on error. For example, capture errors in Sentry, 36 | # # but do not mark the record as invalid. 37 | # validates :password, not_pwned: { 38 | # on_error: ->(record, error) { Raven.capture_exception(error) } 39 | # } 40 | # end 41 | # 42 | # @since 1.2.0 43 | class NotPwnedValidator < ActiveModel::EachValidator 44 | ## 45 | # The default behaviour of this validator in the case of an API failure. The 46 | # default will mean that if the API fails the object will not be marked 47 | # invalid. 48 | DEFAULT_ON_ERROR = :valid 49 | 50 | ## 51 | # The default threshold for whether a breach is considered pwned. The default 52 | # is 0, so any password that appears in a breach will mark the record as 53 | # invalid. 54 | DEFAULT_THRESHOLD = 0 55 | 56 | ## 57 | # Validates the +value+ against the Pwned Passwords API. If the +pwned_count+ 58 | # is higher than the optional +threshold+ then the record is marked as 59 | # invalid. 60 | # 61 | # In the case of an API error the validator will either mark the 62 | # record as valid or invalid. Alternatively it will run an associated proc or 63 | # re-raise the original error. 64 | # 65 | # The validation will short circuit and return with no errors added if the 66 | # password is blank. The +Pwned::Password+ initializer expects the password to 67 | # be a string and will throw a +TypeError+ if it is +nil+. Also, technically 68 | # the empty string is not a password that is reported to be found in data 69 | # breaches, so returns +false+, short circuiting that using +value.blank?+ 70 | # saves us a trip to the API. 71 | # 72 | # @param record [ActiveModel::Validations] The object being validated 73 | # @param attribute [Symbol] The attribute on the record that is currently 74 | # being validated. 75 | # @param value [String] The value of the attribute on the record that is the 76 | # subject of the validation 77 | def validate_each(record, attribute, value) 78 | return if value.blank? 79 | begin 80 | pwned_check = Pwned::Password.new(value, request_options) 81 | if pwned_check.pwned_count > threshold 82 | record.errors.add(attribute, :not_pwned, **options.merge(count: pwned_check.pwned_count)) 83 | end 84 | rescue Pwned::Error => error 85 | case on_error 86 | when :invalid 87 | record.errors.add(attribute, :pwned_error, **options.merge(message: options[:error_message])) 88 | when :valid 89 | # Do nothing, consider the record valid 90 | when Proc 91 | on_error.call(record, error) 92 | else 93 | raise 94 | end 95 | end 96 | end 97 | 98 | private 99 | 100 | def on_error 101 | options[:on_error] || DEFAULT_ON_ERROR 102 | end 103 | 104 | def request_options 105 | options[:request_options] || {} 106 | end 107 | 108 | def threshold 109 | threshold = options[:threshold] || DEFAULT_THRESHOLD 110 | raise TypeError, "#{self.class.to_s} option 'threshold' must be of type Integer" unless threshold.is_a? Integer 111 | threshold 112 | end 113 | end 114 | 115 | ## 116 | # The version 1.1.0 validator that uses `pwned` in the validate method. 117 | # This has been updated to the above `not_pwned` validator to be clearer what 118 | # is being validated. 119 | # 120 | # This class is being maintained for backwards compatibility but will be 121 | # removed 122 | # 123 | # @example Validate a password on a +User+ model with the default options. 124 | # class User < ApplicationRecord 125 | # validates :password, pwned: true 126 | # end 127 | # 128 | # @deprecated use the +NotPwnedValidator+ instead. 129 | # 130 | # @since 1.1.0 131 | class PwnedValidator < NotPwnedValidator 132 | end 133 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for `Pwned` 2 | 3 | ## Ongoing [☰](https://github.com/philnash/pwned/compare/v2.4.0...master) 4 | 5 | ## 2.4.1 (August 29, 2022) [☰](https://github.com/philnash/pwned/compare/v2.4.0...v2.4.1) 6 | 7 | - Minor updates 8 | 9 | - Adds French and Dutch translations 10 | - Adds Rails 7 to the test matrix 11 | 12 | ## 2.4.0 (February 23, 2022) [☰](https://github.com/philnash/pwned/compare/v2.3.0...v2.4.0) 13 | 14 | - Minor updates 15 | 16 | - Adds `default_request_options` to set global defaults for the gem 17 | - Adds Ruby 3.1 to the test matrix 18 | 19 | ## 2.3.0 (August 30, 2021) [☰](https://github.com/philnash/pwned/compare/v2.2.0...v2.3.0) 20 | 21 | - Minor updates 22 | 23 | - Restores `Net::HTTP` default behaviour to use environment supplied HTTP 24 | proxy 25 | - Adds `ignore_env_proxy` to ignore any proxies set in the environment 26 | 27 | ## 2.2.0 (March 27, 2021) [☰](https://github.com/philnash/pwned/compare/v2.1.0...v2.2.0) 28 | 29 | - Minor updates 30 | 31 | - Adds `:proxy` option to `request_options` to directly set a proxy on the 32 | request. Fixes #21, thanks [dparpyani](https://github.com/dparpyani). 33 | 34 | ## 2.1.0 (July 8, 2020) [☰](https://github.com/philnash/pwned/compare/v2.0.2...v2.1.0) 35 | 36 | - Minor updates 37 | 38 | - Adds `Pwned::HashedPassword` class which is initializd with a SHA1 hash to 39 | query the API with so that the lookup can be done in the background without 40 | storing passwords. Fixes #19, thanks [@paprikati](https://github.com/paprikati). 41 | 42 | ## 2.0.2 (May 20, 2020) [☰](https://github.com/philnash/pwned/compare/v2.0.1...v2.0.2) 43 | 44 | - Minor fix 45 | 46 | - It was found to be possible for reading the lines body of a response to 47 | result in a `nil` which caused trouble with string concatenation. This 48 | avoids that scenario. Fixes #18, thanks [@flori](https://github.com/flori). 49 | 50 | ## 2.0.1 (January 14, 2020) [☰](https://github.com/philnash/pwned/compare/v2.0.0...v2.0.1) 51 | 52 | - Minor updates 53 | 54 | - Adds double-splat to ActiveModel::Errors#add calls with options to make Ruby 2.7 happy. 55 | - Detects presence of Net::HTTPClientException in tests to remove deprecation warning. 56 | 57 | ## 2.0.0 (October 1, 2019) [☰](https://github.com/philnash/pwned/compare/v1.2.1...v2.0.0) 58 | 59 | - Major updates 60 | 61 | - Switches from `open-uri` to `Net::HTTP`. This is a potentially breaking change. 62 | - `request_options` are now used to configure `Net::HTTP.start`. 63 | - Rather than using all string keys from `request_options`, HTTP headers are now 64 | specified in their own `headers` hash. To upgrade, any options intended as 65 | headers need to be extracted into a `headers` hash, e.g. 66 | 67 | ```diff 68 | validates :password, not_pwned: { 69 | - request_options: { read_timeout: 5, open_timeout: 1, "User-Agent" => "Super fun user agent" } 70 | + request_options: { read_timeout: 5, open_timeout: 1, headers: { "User-Agent" => "Super fun user agent" } } 71 | } 72 | 73 | - password = Pwned::Password.new("password", 'User-Agent' => 'Super fun new user agent') 74 | + password = Pwned::Password.new("password", headers: { 'User-Agent' => 'Super fun new user agent' }, read_timeout: 10) 75 | ``` 76 | 77 | - Adds a CLI to let you check passwords on the command line 78 | 79 | ```bash 80 | $ pwned password 81 | Pwned! 82 | The password has been found in public breaches 3730471 times. 83 | ``` 84 | 85 | ## 1.2.1 (March 17, 2018) [☰](https://github.com/philnash/pwned/compare/v1.2.0...v1.2.1) 86 | 87 | - Minor updates 88 | - Validator no longer raises `TypeError` when password is `nil` 89 | 90 | ## 1.2.0 (March 15, 2018) [☰](https://github.com/philnash/pwned/compare/v1.1.0...v1.2.0) 91 | 92 | - Major updates 93 | - Changes `PwnedValidator` to `NotPwnedValidator`, so that the validation looks like `validates :password, not_pwned: true`. `PwnedValidator` now subclasses `NotPwnedValidator` for backwards compatibility with version 1.1.0 but is deprecated. 94 | 95 | ## 1.1.0 (March 12, 2018) [☰](https://github.com/philnash/pwned/compare/v1.0.0...v1.1.0) 96 | 97 | - Major updates 98 | 99 | - Refactors exception handling with built in Ruby method ([PR #1](https://github.com/philnash/pwned/pull/1) thanks [@kpumuk](https://github.com/kpumuk)) 100 | - Passwords must be strings, the initializer will raise a `TypeError` unless `password.is_a? String`. ([dbf7697](https://github.com/philnash/pwned/commit/dbf7697e878d87ac74aed1e715cee19b73473369)) 101 | - Added Ruby on Rails validator ([PR #3](https://github.com/philnash/pwned/pull/3) & [PR #6](https://github.com/philnash/pwned/pull/6)) 102 | - Added simplified accessors `Pwned.pwned?` and `Pwned.pwned_count` ([PR #4](https://github.com/philnash/pwned/pull/4)) 103 | 104 | - Minor updates 105 | - SHA1 is only calculated once 106 | - Frozen string literal to make sure Ruby does not copy strings over and over again 107 | - Removal of `@match_data`, since we only use it to retrieve the counter. Caching the counter instead (all [PR #2](https://github.com/philnash/pwned/pull/2) thanks [@kpumuk](https://github.com/kpumuk)) 108 | 109 | ## 1.0.0 (March 6, 2018) [☰](https://github.com/philnash/pwned/commits/v1.0.0) 110 | 111 | Initial release. Includes basic features for checking passwords and their count from the Pwned Passwords API. Allows setting of request headers and other options for open-uri. 112 | -------------------------------------------------------------------------------- /spec/pwned/not_pwned_validator_spec.rb: -------------------------------------------------------------------------------- 1 | class Model 2 | include ActiveModel::Validations 3 | 4 | attr_accessor :password 5 | end 6 | 7 | def create_model(password) 8 | Model.new.tap { |model| model.password = password } 9 | end 10 | 11 | RSpec.describe NotPwnedValidator do 12 | after(:example) do 13 | Model.clear_validators! 14 | end 15 | 16 | describe "when pwned", pwned_range: "5BAA6" do 17 | it "marks the model as invalid" do 18 | Model.validates :password, not_pwned: true 19 | model = create_model("password") 20 | 21 | expect(model).to_not be_valid 22 | expect(model.errors[:password].size).to eq(1) 23 | expect(model.errors[:password].first).to eq("has previously appeared in a data breach and should not be used") 24 | end 25 | 26 | it "allows to change the error message" do 27 | Model.validates :password, not_pwned: { message: "has been pwned %{count} times" } 28 | model = create_model("password") 29 | 30 | expect(model).to_not be_valid 31 | expect(model.errors[:password].size).to eq(1) 32 | expect(model.errors[:password].first).to eq("has been pwned 3303003 times") 33 | end 34 | 35 | it "allows the user agent to be set" do 36 | # Default option should be overridden 37 | Pwned.default_request_options = { headers: { "User-Agent" => "Default user agent" } } 38 | 39 | Model.validates :password, not_pwned: { 40 | request_options: { headers: { "User-Agent" => "Super fun user agent" } } 41 | } 42 | model = create_model("password") 43 | 44 | expect(model).to_not be_valid 45 | expect(a_request(:get, "https://api.pwnedpasswords.com/range/5BAA6"). 46 | with(headers: { "User-Agent" => "Super fun user agent" })). 47 | to have_been_made.once 48 | end 49 | 50 | it "allows the proxy to be set via options" do 51 | # Default option should be overridden 52 | Pwned.default_request_options = { proxy: "https://username:password@default.com:12345" } 53 | 54 | Model.validates :password, not_pwned: { 55 | request_options: { proxy: "https://username:password@example.com:12345" } 56 | } 57 | model = create_model("password") 58 | 59 | # Webmock doesn't support proxy assertions (https://github.com/bblimke/webmock/issues/753) 60 | # so we check that Net::HTTP receives the correct arguments. 61 | expect(Net::HTTP).to receive(:start). 62 | with("api.pwnedpasswords.com", 443, "example.com", 12345, "username", "password", anything). 63 | and_call_original 64 | 65 | expect(model).to_not be_valid 66 | expect(a_request(:get, "https://api.pwnedpasswords.com/range/5BAA6"). 67 | with(headers: { "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })). 68 | to have_been_made.once 69 | end 70 | 71 | it "allows the proxy to be set via default options" do 72 | Pwned.default_request_options = { proxy: "https://username:password@default.com:12345" } 73 | Model.validates :password, not_pwned: true 74 | model = create_model("password") 75 | 76 | # Webmock doesn't support proxy assertions (https://github.com/bblimke/webmock/issues/753) 77 | # so we check that Net::HTTP receives the correct arguments. 78 | expect(Net::HTTP).to receive(:start). 79 | with("api.pwnedpasswords.com", 443, "default.com", 12345, "username", "password", anything). 80 | and_call_original 81 | 82 | expect(model).to_not be_valid 83 | expect(a_request(:get, "https://api.pwnedpasswords.com/range/5BAA6"). 84 | with(headers: { "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })). 85 | to have_been_made.once 86 | end 87 | end 88 | 89 | describe "when not pwned", pwned_range: "37D5B" do 90 | it "reports the model as valid" do 91 | Model.validates :password, not_pwned: true 92 | model = create_model("t3hb3stpa55w0rd") 93 | 94 | expect(model).to be_valid 95 | end 96 | end 97 | 98 | describe "with a threshold for pwned count", pwned_range: "613D1" do 99 | it "reports the model as invalid when pwned count is above threshold" do 100 | Model.validates :password, not_pwned: { threshold: 1 } 101 | model = create_model("harlequin10") 102 | 103 | expect(model).to_not be_valid 104 | end 105 | 106 | it "reports the model as valid when pwned count is below threshold" do 107 | Model.validates :password, not_pwned: { threshold: 10 } 108 | model = create_model("harlequin10") 109 | 110 | expect(model).to be_valid 111 | end 112 | 113 | it "expects threshold to be an integer" do 114 | Model.validates :password, not_pwned: { threshold: "10" } 115 | model = create_model("harlequin10") 116 | 117 | expect { model.valid? }.to raise_error(TypeError, /NotPwnedValidator option 'threshold'/) 118 | end 119 | end 120 | 121 | describe "when the API times out" do 122 | before(:example) do 123 | @stub = stub_request(:get, "https://api.pwnedpasswords.com/range/5BAA6").to_timeout 124 | end 125 | 126 | it "marks the model as valid when not error handling configured" do 127 | Model.validates :password, not_pwned: true 128 | model = create_model("password") 129 | 130 | expect(model).to be_valid 131 | end 132 | 133 | it "raises a custom error when error handling configured to :raise_error" do 134 | Model.validates :password, not_pwned: { on_error: :raise_error } 135 | model = create_model("password") 136 | 137 | expect { model.valid? }.to raise_error(Pwned::TimeoutError, /execution expired/) 138 | end 139 | 140 | it "marks the model as invalid when error handling configured to :invalid" do 141 | Model.validates :password, not_pwned: { on_error: :invalid } 142 | model = create_model("password") 143 | 144 | expect(model).to_not be_valid 145 | expect(model.errors[:password].size).to eq(1) 146 | expect(model.errors[:password].first).to eq("could not be verified against the past data breaches") 147 | end 148 | 149 | it "marks the model as invalid with a custom error message when error handling configured to :invalid" do 150 | Model.validates :password, not_pwned: { on_error: :invalid, error_message: "might be pwned" } 151 | model = create_model("password") 152 | 153 | expect(model).to_not be_valid 154 | expect(model.errors[:password].size).to eq(1) 155 | expect(model.errors[:password].first).to eq("might be pwned") 156 | end 157 | 158 | it "marks the model as valid when error handling configured to :valid" do 159 | Model.validates :password, not_pwned: { on_error: :valid } 160 | model = create_model("password") 161 | 162 | expect(model).to be_valid 163 | end 164 | 165 | it "calls a proc configured for error handling" do 166 | Model.validates :password, not_pwned: { on_error: ->(record, error) { raise RuntimeError, "custom proc" } } 167 | model = create_model("password") 168 | 169 | expect { model.valid? }.to raise_error(RuntimeError, "custom proc") 170 | end 171 | end 172 | 173 | describe "when the model's password is not present" do 174 | it "is valid with a `nil` password" do 175 | Model.validates :password, not_pwned: true 176 | model = Model.new 177 | expect(model).to be_valid 178 | end 179 | 180 | it "is valid with a `blank` password" do 181 | Model.validates :password, not_pwned: true 182 | model = Model.new 183 | model.password = "" 184 | expect(model).to be_valid 185 | end 186 | end 187 | end 188 | 189 | # Supports the 1.1.0 `pwned: true` validation. Should be removed eventually. 190 | RSpec.describe PwnedValidator do 191 | after(:example) do 192 | Model.clear_validators! 193 | end 194 | 195 | describe "when pwned", pwned_range: "5BAA6" do 196 | it "marks the model as invalid" do 197 | Model.validates :password, pwned: true 198 | model = create_model("password") 199 | 200 | expect(model).to_not be_valid 201 | expect(model.errors[:password].size).to eq(1) 202 | expect(model.errors[:password].first).to eq("has previously appeared in a data breach and should not be used") 203 | end 204 | end 205 | 206 | describe "when not pwned", pwned_range: "37D5B" do 207 | it "reports the model as valid" do 208 | Model.validates :password, pwned: true 209 | model = create_model("t3hb3stpa55w0rd") 210 | 211 | expect(model).to be_valid 212 | end 213 | end 214 | end 215 | -------------------------------------------------------------------------------- /spec/pwned/password_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Pwned::Password do 2 | let(:password) { Pwned::Password.new("password") } 3 | 4 | it "initializes with a password" do 5 | expect(password.password).to eq("password") 6 | end 7 | 8 | it "doesn't initialize with an integer" do 9 | expect { Pwned::Password.new(123) }.to raise_error(TypeError) 10 | end 11 | 12 | it "doesn't initialize with an array" do 13 | expect { Pwned::Password.new(["hello", "world"]) }.to raise_error(TypeError) 14 | end 15 | 16 | it "doesn't initialize with a hash" do 17 | expect { Pwned::Password.new({ a: "b", c: "d" }) }.to raise_error(TypeError) 18 | end 19 | 20 | it "has a hashed version of the password" do 21 | expect(password.hashed_password).to eq("5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8") 22 | end 23 | 24 | describe "when pwned", pwned_range: "5BAA6" do 25 | it "reports it is pwned" do 26 | expect(password.pwned?).to be true 27 | expect(@stub).to have_been_requested 28 | end 29 | 30 | it "reports it has been pwned many times" do 31 | expect(password.pwned_count).to eq(3303003) 32 | expect(@stub).to have_been_requested 33 | end 34 | 35 | it "hashes password once" do 36 | expect(Digest::SHA1).to receive(:hexdigest).once.and_call_original 37 | expect(password.pwned?).to be true 38 | expect(@stub).to have_been_requested 39 | end 40 | 41 | it "works with simplified accessors" do 42 | expect(Pwned.pwned?(password.password)).to be true 43 | expect(Pwned.pwned_count(password.password)).to eq(3303003) 44 | end 45 | end 46 | 47 | describe "when not pwned", pwned_range: "37D5B" do 48 | let(:password) { Pwned::Password.new("t3hb3stpa55w0rd") } 49 | 50 | it "reports it is not pwned" do 51 | expect(password.pwned?).to be false 52 | expect(@stub).to have_been_requested 53 | end 54 | 55 | it "reports it has been pwned zero times" do 56 | expect(password.pwned_count).to eq(0) 57 | expect(@stub).to have_been_requested 58 | end 59 | 60 | it "works with simplified accessors" do 61 | expect(Pwned.pwned?(password.password)).to be false 62 | expect(Pwned.pwned_count(password.password)).to eq(0) 63 | end 64 | end 65 | 66 | describe "when the API times out" do 67 | before(:example) do 68 | @stub = stub_request(:get, "https://api.pwnedpasswords.com/range/5BAA6").to_timeout 69 | end 70 | 71 | it "raises a custom error" do 72 | expect { password.pwned? }.to raise_error(&method(:verify_timeout_error)) 73 | expect { password.pwned_count }.to raise_error(&method(:verify_timeout_error)) 74 | expect(@stub).to have_been_requested.times(2) 75 | end 76 | 77 | def verify_timeout_error(error) 78 | aggregate_failures "testing custom error" do 79 | expect(error).to be_kind_of(Pwned::TimeoutError) 80 | expect(error.message).to match(/execution expired/) 81 | expect(error.cause).to be_kind_of(Net::OpenTimeout) 82 | end 83 | end 84 | end 85 | 86 | describe "when the API returns an error" do 87 | before(:example) do 88 | @stub = stub_request(:get, "https://api.pwnedpasswords.com/range/5BAA6").to_return(status: 500) 89 | end 90 | 91 | it "raises a custom error" do 92 | expect { password.pwned? }.to raise_error(&method(:verify_internal_error)) 93 | expect { password.pwned_count }.to raise_error(&method(:verify_internal_error)) 94 | expect(@stub).to have_been_requested.times(2) 95 | end 96 | 97 | def verify_internal_error(error) 98 | aggregate_failures "testing custom error" do 99 | expect(error).to be_kind_of(Pwned::Error) 100 | expect(error.message).to match(/500/) 101 | expect(error.cause).to be_kind_of(Net::HTTPFatalError) 102 | end 103 | end 104 | end 105 | 106 | describe "when the API returns a 404" do 107 | # It shouldn't return a 404, but this tests it anyway. 108 | before(:example) do 109 | @stub = stub_request(:get, "https://api.pwnedpasswords.com/range/5BAA6").to_return(status: 404) 110 | end 111 | 112 | it "raises a custom error" do 113 | expect { password.pwned? }.to raise_error(&method(:verify_not_found_error)) 114 | expect { password.pwned_count }.to raise_error(&method(:verify_not_found_error)) 115 | expect(@stub).to have_been_requested.times(2) 116 | end 117 | 118 | def verify_not_found_error(error) 119 | aggregate_failures "testing custom error" do 120 | expect(error).to be_kind_of(Pwned::Error) 121 | expect(error.message).to match(/404/) 122 | if Object.const_defined?("Net::HTTPClientException") 123 | # Net::HTTPServerException is deprecated in favour of 124 | # Net::HTTPClientException. More detail here: 125 | # https://bugs.ruby-lang.org/issues/14688 126 | expect(error.cause).to be_kind_of(Net::HTTPClientException) 127 | else 128 | expect(error.cause).to be_kind_of(Net::HTTPServerException) 129 | end 130 | end 131 | end 132 | end 133 | 134 | describe "advanced requests", pwned_range: "5BAA6" do 135 | it "sends a user agent with the current version" do 136 | password.pwned? 137 | 138 | expect(a_request(:get, "https://api.pwnedpasswords.com/range/5BAA6"). 139 | with(headers: { "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })). 140 | to have_been_made.once 141 | end 142 | 143 | it "allows the user agent to be set in constructor" do 144 | Pwned.default_request_options = { headers: { "User-Agent" => "Default user agent" } } 145 | password = Pwned::Password.new("password", headers: { "User-Agent" => "Super fun user agent" }) 146 | password.pwned? 147 | 148 | expect(a_request(:get, "https://api.pwnedpasswords.com/range/5BAA6"). 149 | with(headers: { "User-Agent" => "Super fun user agent" })). 150 | to have_been_made.once 151 | end 152 | 153 | it "allows the user agent to be set with default settings" do 154 | Pwned.default_request_options = { headers: { "User-Agent" => "Default user agent" } } 155 | password = Pwned::Password.new("password") 156 | password.pwned? 157 | 158 | expect(a_request(:get, "https://api.pwnedpasswords.com/range/5BAA6"). 159 | with(headers: { "User-Agent" => "Default user agent" })). 160 | to have_been_made.once 161 | end 162 | 163 | it "allows headers to be set by default or in the constructor and merges them" do 164 | Pwned.default_request_options = { headers: { "User-Agent" => "Default user agent" } } 165 | password = Pwned::Password.new("password", headers: { "X-Test-Header" => "this-is-a-test" }) 166 | password.pwned? 167 | 168 | expect(a_request(:get, "https://api.pwnedpasswords.com/range/5BAA6"). 169 | with(headers: { "User-Agent" => "Default user agent", "X-Test-Header" => "this-is-a-test" })). 170 | to have_been_made.once 171 | end 172 | 173 | let(:subject) { Pwned::Password.new("password", request_options).pwned? } 174 | 175 | let(:request_options) { {} } 176 | let(:environment_proxy) { "https://username:password@environment.com:12345" } 177 | let(:explicit_proxy) { "https://username:password@explicit.com:56789" } 178 | 179 | shared_examples_for "uses explicit proxy" do 180 | it "uses proxy from request options" do 181 | expect(Net::HTTP).to receive(:start).and_wrap_original do |original_method, *args, &block| 182 | http = original_method.call(*args) 183 | expect(http.proxy_from_env?).to eq(false) 184 | expect(http.proxy_address).to eq("explicit.com") 185 | expect(http.proxy_user).to eq("username") 186 | expect(http.proxy_pass).to eq("password") 187 | expect(http.proxy_port).to eq(56_789) 188 | original_method.call(*args, &block) 189 | end 190 | 191 | subject 192 | 193 | expect(a_request(:get, "https://api.pwnedpasswords.com/range/5BAA6") 194 | .with(headers: { "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })) 195 | .to have_been_made.once 196 | end 197 | end 198 | 199 | shared_examples_for "doesn't use proxy from environment" do 200 | context "explicit proxy is given" do 201 | before { request_options[:proxy] = explicit_proxy } 202 | include_examples "uses explicit proxy" 203 | end 204 | 205 | context "explicit proxy not given" do 206 | before { request_options.delete(:proxy) } 207 | 208 | it "doesn't use a proxy" do 209 | expect(Net::HTTP).to receive(:start).and_wrap_original do |original_method, *args, &block| 210 | http = original_method.call(*args) 211 | expect(http.proxy?).to eq(false) 212 | original_method.call(*args, &block) 213 | end 214 | 215 | subject 216 | 217 | expect(a_request(:get, "https://api.pwnedpasswords.com/range/5BAA6") 218 | .with(headers: { "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })) 219 | .to have_been_made.once 220 | end 221 | end 222 | end 223 | 224 | shared_examples_for "uses proxy from environment" do 225 | context "proxy not given in request options" do 226 | let(:request_options) { {} } 227 | 228 | it "uses proxy from the environment" do 229 | expect(Net::HTTP).to receive(:start).and_wrap_original do |original_method, *args, &block| 230 | http = original_method.call(*args) 231 | expect(http.proxy_from_env?).to eq(true) 232 | expect(http.proxy_address).to eq("environment.com") 233 | expect(http.proxy_user).to eq("username") 234 | expect(http.proxy_pass).to eq("password") 235 | expect(http.proxy_port).to eq(12_345) 236 | original_method.call(*args, &block) 237 | end 238 | 239 | subject 240 | 241 | expect(a_request(:get, "https://api.pwnedpasswords.com/range/5BAA6") 242 | .with(headers: { "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })) 243 | .to have_been_made.once 244 | end 245 | end 246 | end 247 | 248 | context "proxy exists in environment" do 249 | before { ENV["http_proxy"] = environment_proxy } 250 | 251 | context "ignore_env_proxy is not given" do 252 | before { request_options.delete(:ignore_env_proxy) } 253 | 254 | context "proxy is given in request options" do 255 | before { request_options[:proxy] = explicit_proxy } 256 | include_examples "uses explicit proxy" 257 | end 258 | 259 | include_examples "uses proxy from environment" 260 | end 261 | 262 | context "ignore_env_proxy is false" do 263 | before { request_options[:ignore_env_proxy] = false } 264 | 265 | context "proxy is given in request options" do 266 | before { request_options[:proxy] = explicit_proxy } 267 | include_examples "uses explicit proxy" 268 | end 269 | 270 | include_examples "uses proxy from environment" 271 | end 272 | 273 | context "ignore_env_proxy is true" do 274 | before { request_options[:ignore_env_proxy] = true } 275 | 276 | include_examples "doesn't use proxy from environment" 277 | end 278 | end 279 | 280 | context "proxy environment variable does not exist" do 281 | before { ENV["http_proxy"] = nil } 282 | 283 | context "ignore_env_proxy is not given" do 284 | before { request_options.delete(:ignore_env_proxy) } 285 | include_examples "doesn't use proxy from environment" 286 | end 287 | 288 | context "ignore_env_proxy is true" do 289 | before { request_options[:ignore_env_proxy] = true } 290 | include_examples "doesn't use proxy from environment" 291 | end 292 | 293 | context "ignore_env_proxy is false" do 294 | before { request_options[:ignore_env_proxy] = false } 295 | include_examples "doesn't use proxy from environment" 296 | end 297 | end 298 | 299 | context "proxy given in default request options" do 300 | before { Pwned.default_request_options = { proxy: "https://username:password@default.com:12345" } } 301 | 302 | it "uses proxy from the default require options" do 303 | expect(Net::HTTP).to receive(:start).and_wrap_original do |original_method, *args, &block| 304 | http = original_method.call(*args) 305 | expect(http.proxy_from_env?).to eq(false) 306 | expect(http.proxy_address).to eq("default.com") 307 | expect(http.proxy_user).to eq("username") 308 | expect(http.proxy_pass).to eq("password") 309 | expect(http.proxy_port).to eq(12_345) 310 | original_method.call(*args, &block) 311 | end 312 | 313 | subject 314 | 315 | expect(a_request(:get, "https://api.pwnedpasswords.com/range/5BAA6") 316 | .with(headers: { "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })) 317 | .to have_been_made.once 318 | end 319 | end 320 | end 321 | 322 | describe "streaming", pwned_range: "A0F41" do 323 | let(:password) { Pwned::Password.new("fake-password") } 324 | 325 | # Since our streaming is yielding by line across chunks, ensure we're not 326 | # missing lines by checking a single line file 327 | it "streams the whole file" do 328 | expect(password).to be_pwned 329 | end 330 | 331 | it "works when response stream returns several empty chunks" do 332 | response = double 333 | allow(response).to receive(:read_body). 334 | and_yield(""). 335 | and_yield(""). 336 | and_yield("hello\nworld\n") 337 | 338 | password.send(:stream_response_lines, response) do |line| 339 | expect(line).to eq("hello\n") | eq("world\n") 340 | end 341 | end 342 | end 343 | 344 | describe "empty response", pwned_range: "AD871" do 345 | let(:password) { Pwned::Password.new("empty") } 346 | 347 | it "is not pwned" do 348 | expect(password).not_to be_pwned 349 | end 350 | end 351 | end 352 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pwned 2 | 3 | An easy, Ruby way to use the Pwned Passwords API. 4 | 5 | [![Gem Version](https://badge.fury.io/rb/pwned.svg)](https://rubygems.org/gems/pwned) ![Build Status](https://github.com/philnash/pwned/workflows/tests/badge.svg) [![Maintainability](https://codeclimate.com/github/philnash/pwned/badges/gpa.svg)](https://codeclimate.com/github/philnash/pwned/maintainability) [![Inline docs](https://inch-ci.org/github/philnash/pwned.svg?branch=master)](https://inch-ci.org/github/philnash/pwned) 6 | 7 | [API docs](https://www.rubydoc.info/gems/pwned) | [GitHub repository](https://github.com/philnash/pwned) 8 | 9 | ## Table of Contents 10 | 11 | * [Table of Contents](#table-of-contents) 12 | * [About](#about) 13 | * [Installation](#installation) 14 | * [Usage](#usage) 15 | * [Plain Ruby](#plain-ruby) 16 | * [Custom request options](#custom-request-options) 17 | * [HTTP Headers](#http-headers) 18 | * [HTTP Proxy](#http-proxy) 19 | * [ActiveRecord Validator](#activerecord-validator) 20 | * [I18n](#i18n) 21 | * [Threshold](#threshold) 22 | * [Network Error Handling](#network-error-handling) 23 | * [Custom Request Options](#custom-request-options-1) 24 | * [HTTP Headers](#http-headers-1) 25 | * [HTTP Proxy](#http-proxy-1) 26 | * [Using Asynchronously](#using-asynchronously) 27 | * [Devise](#devise) 28 | * [Rodauth](#rodauth) 29 | * [Command line](#command-line) 30 | * [Unpwn](#unpwn) 31 | * [How Pwned is Pi?](#how-pwned-is-pi) 32 | * [Development](#development) 33 | * [Contributing](#contributing) 34 | * [License](#license) 35 | * [Code of Conduct](#code-of-conduct) 36 | 37 | ## About 38 | 39 | Troy Hunt's [Pwned Passwords API](https://haveibeenpwned.com/API/v3#PwnedPasswords) allows you to check if a password has been found in any of the huge data breaches. 40 | 41 | `Pwned` is a Ruby library to use the Pwned Passwords API's [k-Anonymity model](https://www.troyhunt.com/ive-just-launched-pwned-passwords-version-2/#cloudflareprivacyandkanonymity) to test a password against the API without sending the entire password to the service. 42 | 43 | The data from this API is provided by [Have I been pwned?](https://haveibeenpwned.com/). Before using the API, please check [the acceptable uses and license of the API](https://haveibeenpwned.com/API/v3#AcceptableUse). 44 | 45 | Here is a blog post I wrote on [how to use this gem in your Ruby applications to make your users' passwords better](https://www.twilio.com/blog/2018/03/better-passwords-in-ruby-applications-pwned-passwords-api.html). 46 | 47 | ## Installation 48 | 49 | Add this line to your application's Gemfile: 50 | 51 | ```ruby 52 | gem 'pwned' 53 | ``` 54 | 55 | And then execute: 56 | 57 | $ bundle 58 | 59 | Or install it yourself as: 60 | 61 | $ gem install pwned 62 | 63 | ## Usage 64 | 65 | There are a few ways you can use this gem: 66 | 67 | 1. [Plain Ruby](#plain-ruby) 68 | 2. [Rails](#activerecord-validator) 69 | 3. [Rails and Devise](#devise) 70 | 71 | ### Plain Ruby 72 | 73 | To test a password against the API, instantiate a `Pwned::Password` object and then ask if it is `pwned?`. 74 | 75 | ```ruby 76 | password = Pwned::Password.new("password") 77 | password.pwned? 78 | #=> true 79 | password.pwned_count 80 | #=> 3303003 81 | ``` 82 | 83 | You can also check how many times the password appears in the dataset. 84 | 85 | ```ruby 86 | password = Pwned::Password.new("password") 87 | password.pwned_count 88 | #=> 3303003 89 | ``` 90 | 91 | Since you are likely using this as part of a sign-up flow, it is recommended that you rescue errors so if the service does go down, your user journey is not disturbed. 92 | 93 | ```ruby 94 | begin 95 | password = Pwned::Password.new("password") 96 | password.pwned? 97 | rescue Pwned::Error => e 98 | # Ummm... don't worry about it, I guess? 99 | end 100 | ``` 101 | 102 | Most of the times you only care if the password has been pwned before or not. You can use simplified accessors to check whether the password has been pwned, or how many times it was pwned: 103 | 104 | ```ruby 105 | Pwned.pwned?("password") 106 | #=> true 107 | Pwned.pwned_count("password") 108 | #=> 3303003 109 | ``` 110 | 111 | #### Custom request options 112 | 113 | You can set HTTP request options to be used with `Net::HTTP.start` when making the request to the API. These options are documented in the [`Net::HTTP.start` documentation](https://ruby-doc.org/stdlib-3.0.0/libdoc/net/http/rdoc/Net/HTTP.html#method-c-start). 114 | 115 | You can pass the options to the constructor: 116 | 117 | ```ruby 118 | password = Pwned::Password.new("password", read_timeout: 10) 119 | ``` 120 | 121 | You can also specify global defaults: 122 | 123 | ```ruby 124 | Pwned.default_request_options = { read_timeout: 10 } 125 | ``` 126 | 127 | ##### HTTP Headers 128 | 129 | The `:headers` option defines HTTP headers. These headers must be string keys. 130 | 131 | ```ruby 132 | password = Pwned::Password.new("password", headers: { 133 | 'User-Agent' => 'Super fun new user agent' 134 | }) 135 | ``` 136 | 137 | ##### HTTP Proxy 138 | 139 | An HTTP proxy can be set using the `http_proxy` or `HTTP_PROXY` environment variable. This is the same way that `Net::HTTP` handles HTTP proxies if no proxy options are given. See [`URI::Generic#find_proxy`](https://ruby-doc.org/stdlib-3.0.1/libdoc/uri/rdoc/URI/Generic.html#method-i-find_proxy) for full details on how Ruby detects a proxy from the environment. 140 | 141 | ```ruby 142 | # Set in the environment 143 | ENV["http_proxy"] = "https://username:password@example.com:12345" 144 | 145 | # Will use the above proxy 146 | password = Pwned::Password.new("password") 147 | ``` 148 | 149 | You can specify a custom HTTP proxy with the `:proxy` option: 150 | 151 | ```ruby 152 | password = Pwned::Password.new( 153 | "password", 154 | proxy: "https://username:password@example.com:12345" 155 | ) 156 | ``` 157 | 158 | If you don't want to set a proxy and you don't want a proxy to be inferred from the environment, set the `:ignore_env_proxy` key: 159 | 160 | ```ruby 161 | password = Pwned::Password.new("password", ignore_env_proxy: true) 162 | ``` 163 | 164 | ### ActiveRecord Validator 165 | 166 | There is a custom validator available for your ActiveRecord models: 167 | 168 | ```ruby 169 | class User < ApplicationRecord 170 | validates :password, not_pwned: true 171 | # or 172 | validates :password, not_pwned: { message: "has been pwned %{count} times" } 173 | end 174 | ``` 175 | 176 | #### I18n 177 | 178 | You can change the error message using I18n (use `%{count}` to interpolate the number of times the password was seen in the data breaches): 179 | 180 | ```yaml 181 | en: 182 | errors: 183 | messages: 184 | not_pwned: has been pwned %{count} times 185 | pwned_error: might be pwned 186 | ``` 187 | 188 | #### Threshold 189 | 190 | If you are ok with the password appearing a certain number of times before you decide it is invalid, you can set a threshold. The validator will check whether the `pwned_count` is greater than the threshold. 191 | 192 | ```ruby 193 | class User < ApplicationRecord 194 | # The record is marked as valid if the password has been used once in the breached data 195 | validates :password, not_pwned: { threshold: 1 } 196 | end 197 | ``` 198 | 199 | #### Network Error Handling 200 | 201 | By default the record will be treated as valid when we cannot reach the [haveibeenpwned.com](https://haveibeenpwned.com/) servers. This can be changed with the `:on_error` validator parameter: 202 | 203 | ```ruby 204 | class User < ApplicationRecord 205 | # The record is marked as valid on network errors. 206 | validates :password, not_pwned: true 207 | validates :password, not_pwned: { on_error: :valid } 208 | 209 | # The record is marked as invalid on network errors 210 | # (error message "could not be verified against the past data breaches".) 211 | validates :password, not_pwned: { on_error: :invalid } 212 | 213 | # The record is marked as invalid on network errors with custom error. 214 | validates :password, not_pwned: { on_error: :invalid, error_message: "might be pwned" } 215 | 216 | # We will raise an error on network errors. 217 | # This means that `record.valid?` will raise `Pwned::Error`. 218 | # Not recommended to use in production. 219 | validates :password, not_pwned: { on_error: :raise_error } 220 | 221 | # Call custom proc on error. For example, capture errors in Sentry, 222 | # but do not mark the record as invalid. 223 | validates :password, not_pwned: { 224 | on_error: ->(record, error) { Raven.capture_exception(error) } 225 | } 226 | end 227 | ``` 228 | 229 | #### Custom Request Options 230 | 231 | You can configure network requests made from the validator using `:request_options` (see [Net::HTTP.start](http://ruby-doc.org/stdlib-2.6.3/libdoc/net/http/rdoc/Net/HTTP.html#method-c-start) for the list of available options). 232 | 233 | ```ruby 234 | validates :password, not_pwned: { 235 | request_options: { 236 | read_timeout: 5, 237 | open_timeout: 1 238 | } 239 | } 240 | ``` 241 | 242 | These options override the globally defined default options (see above). 243 | 244 | In addition to these options, you can also set the following: 245 | 246 | ##### HTTP Headers 247 | 248 | HTTP headers can be specified with the `:headers` key (e.g. `"User-Agent"`) 249 | 250 | ```ruby 251 | validates :password, not_pwned: { 252 | request_options: { 253 | headers: { "User-Agent" => "Super fun user agent" } 254 | } 255 | } 256 | ``` 257 | 258 | ##### HTTP Proxy 259 | 260 | An HTTP proxy can be set using the `http_proxy` or `HTTP_PROXY` environment variable. This is the same way that `Net::HTTP` handles HTTP proxies if no proxy options are given. See [`URI::Generic#find_proxy`](https://ruby-doc.org/stdlib-3.0.1/libdoc/uri/rdoc/URI/Generic.html#method-i-find_proxy) for full details on how Ruby detects a proxy from the environment. 261 | 262 | ```ruby 263 | # Set in the environment 264 | ENV["http_proxy"] = "https://username:password@example.com:12345" 265 | 266 | validates :password, not_pwned: true 267 | ``` 268 | 269 | You can specify a custom HTTP proxy with the `:proxy` key: 270 | 271 | ```ruby 272 | validates :password, not_pwned: { 273 | request_options: { 274 | proxy: "https://username:password@example.com:12345" 275 | } 276 | } 277 | ``` 278 | 279 | If you don't want to set a proxy and you don't want a proxy to be inferred from the environment, set the `:ignore_env_proxy` key: 280 | 281 | ```ruby 282 | validates :password, not_pwned: { 283 | request_options: { 284 | ignore_env_proxy: true 285 | } 286 | } 287 | ``` 288 | 289 | ### Using Asynchronously 290 | 291 | You may have a use case for hashing the password in advance, and then making the call to the Pwned Passwords API later (for example if you want to enqueue a job without storing the plaintext password). To do this, you can hash the password with the `Pwned.hash_password` method and then initialize the `Pwned::HashedPassword` class with the hash, like this: 292 | 293 | ```ruby 294 | hashed_password = Pwned.hash_password(password) 295 | # some time later 296 | Pwned::HashedPassword.new(hashed_password, request_options).pwned? 297 | ``` 298 | 299 | The `Pwned::HashedPassword` constructor takes all the same options as the regular `Pwned::Password` constructor. 300 | 301 | ### Devise 302 | 303 | If you are using [Devise](https://github.com/heartcombo/devise) I recommend you use the [devise-pwned_password extension](https://github.com/michaelbanfield/devise-pwned_password) which is now powered by this gem. 304 | 305 | ### Rodauth 306 | 307 | If you are using [Rodauth](https://github.com/jeremyevans/rodauth) then you can use the [rodauth-pwned](https://github.com/janko/rodauth-pwned) feature which is powered by this gem. 308 | 309 | ### Command line 310 | 311 | The gem provides a command line utility for checking passwords. You can call it from your terminal application like this: 312 | 313 | ```bash 314 | $ pwned password 315 | Pwned! 316 | The password has been found in public breaches 3645804 times. 317 | ``` 318 | 319 | If you don't want the password you are checking to be visible, call: 320 | 321 | ```bash 322 | $ pwned --secret 323 | ``` 324 | 325 | You will be prompted for the password, but it won't be displayed. 326 | 327 | ### Unpwn 328 | 329 | To cut down on unnecessary network requests, [the unpwn project](https://github.com/indirect/unpwn) uses a list of the top one million passwords to check passwords against. Only if a password is not included in the top million is it then checked against the Pwned Passwords API. 330 | 331 | ## How Pwned is Pi? 332 | 333 | [@daz](https://github.com/daz) [shared](https://twitter.com/dazonic/status/1074647842046660609) a fantastic example of using this gem to show how many times the digits of Pi have been used as passwords and leaked. 334 | 335 | ```ruby 336 | require 'pwned' 337 | 338 | PI = '3.14159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848111' 339 | 340 | for n in 1..40 341 | password = Pwned::Password.new PI[0..(n + 1)] 342 | str = [ n.to_s.rjust(2) ] 343 | str << (password.pwned? ? '😡' : '😃') 344 | str << password.pwned_count.to_s.rjust(4) 345 | str << password.password 346 | 347 | puts str.join ' ' 348 | end 349 | ``` 350 | 351 | The results may, or may not, surprise you. 352 | 353 | ``` 354 | 1 😡 16 3.1 355 | 2 😡 238 3.14 356 | 3 😡 34 3.141 357 | 4 😡 1345 3.1415 358 | 5 😡 2552 3.14159 359 | 6 😡 791 3.141592 360 | 7 😡 9582 3.1415926 361 | 8 😡 1591 3.14159265 362 | 9 😡 637 3.141592653 363 | 10 😡 873 3.1415926535 364 | 11 😡 137 3.14159265358 365 | 12 😡 103 3.141592653589 366 | 13 😡 65 3.1415926535897 367 | 14 😡 201 3.14159265358979 368 | 15 😡 41 3.141592653589793 369 | 16 😡 57 3.1415926535897932 370 | 17 😡 28 3.14159265358979323 371 | 18 😡 29 3.141592653589793238 372 | 19 😡 1 3.1415926535897932384 373 | 20 😡 7 3.14159265358979323846 374 | 21 😡 5 3.141592653589793238462 375 | 22 😡 2 3.1415926535897932384626 376 | 23 😡 2 3.14159265358979323846264 377 | 24 😃 0 3.141592653589793238462643 378 | 25 😡 3 3.1415926535897932384626433 379 | 26 😃 0 3.14159265358979323846264338 380 | 27 😃 0 3.141592653589793238462643383 381 | 28 😃 0 3.1415926535897932384626433832 382 | 29 😃 0 3.14159265358979323846264338327 383 | 30 😃 0 3.141592653589793238462643383279 384 | 31 😃 0 3.1415926535897932384626433832795 385 | 32 😃 0 3.14159265358979323846264338327950 386 | 33 😃 0 3.141592653589793238462643383279502 387 | 34 😃 0 3.1415926535897932384626433832795028 388 | 35 😃 0 3.14159265358979323846264338327950288 389 | 36 😃 0 3.141592653589793238462643383279502884 390 | 37 😃 0 3.1415926535897932384626433832795028841 391 | 38 😃 0 3.14159265358979323846264338327950288419 392 | 39 😃 0 3.141592653589793238462643383279502884197 393 | 40 😃 0 3.1415926535897932384626433832795028841971 394 | ``` 395 | 396 | ## Development 397 | 398 | After checking out the repository, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 399 | 400 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 401 | 402 | ## Contributing 403 | 404 | Bug reports and pull requests are welcome on GitHub at https://github.com/philnash/pwned. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 405 | 406 | ## License 407 | 408 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 409 | 410 | ## Code of Conduct 411 | 412 | Everyone interacting in the Pwned project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/philnash/pwned/blob/master/CODE_OF_CONDUCT.md). 413 | -------------------------------------------------------------------------------- /spec/fixtures/5BAA6.txt: -------------------------------------------------------------------------------- 1 | 003D68EB55068C33ACE09247EE4C639306B:3 2 | 012C192B2F16F82EA0EB9EF18D9D539B0DD:1 3 | 01330C689E5D64F660D6947A93AD634EF8F:1 4 | 0198748F3315F40B1A102BF18EEA0194CD9:1 5 | 01F9033B3C00C65DBFD6D1DC4D22918F5E9:2 6 | 0424DB98C7A0846D2C6C75E697092A0CC3E:5 7 | 047F229A81EE2747253F9897DA38946E241:1 8 | 04A37A676E312CC7C4D236C93FBD992AA3C:4 9 | 04AE045B134BDC43043B216AEF66100EE00:2 10 | 0502EA98ED7A1000D932B10F7707D37FFB4:5 11 | 0539F86F519AACC7030B728CD47803E5B22:5 12 | 054A0BD53E2BC83A87EFDC236E2D0498C08:3 13 | 05AA835DC9423327DAEC1CBD38FA99B8834:1 14 | 05E0182DEAE22D02F6ED35280BCAC370179:4 15 | 078FF3B0C6DD716047976D7D7569667E61C:1 16 | 07956D897C9204261693E1C8908FA12D30E:1 17 | 07A7225B7774BF2796FD3F7A19C17198B5C:1 18 | 07B20D697D2341BEB3EB9CBF0A55C82D5FB:2 19 | 07EE3C8C863CAD50CA4795A7FF79E3EBBC9:5 20 | 0871A33287C74B37B7A6FE7E57CA96A1490:1 21 | 09C127E05A5F032E0F2E942C676DD4F5D71:2 22 | 0B25AA3CF151CD4D0EA4BD8A1243A9B00AD:2 23 | 0C54A78F6E71DCA1D15EF76864E565FB1F0:1 24 | 0C58C8D59A7C8C15BCF29EC2F3BC90BC1F6:2 25 | 0CBAEBA062F6639F3A062CC239EBA525DA3:1 26 | 0F1939EC5AD48A58879C46E069768F5E7A7:1 27 | 0F8C72A929F304D332F933A742337615FE5:3 28 | 0F8F7CEC9FB03196A96A16F3B10B02CF296:7 29 | 0FDFD37BB4EB84FAABB1980435E6E1F2885:3 30 | 11686E89E2FC9359ECD6F722800B90F093B:5 31 | 1170228E0FD2380A57EA6B7722E277FB8D4:2 32 | 11C9990E10807FAD895158DECBA900FD210:3 33 | 11F662D179759E8D4AEE33085E21C4577C8:2 34 | 12D980DB77C653E89C27AA5F757E1866BF3:3 35 | 1306B451DC880C7E4C69B648E8EDC488EB9:2 36 | 134F847708F2CA9B9C269EB1F731E32F3F6:2 37 | 13507FD5AAE15C9EBB2936F5807954A03F1:1 38 | 1374461EB91969C27AC69C0FCAA19D491EA:3 39 | 13C49876A8E2DF99A7A5F1ADA0AED3B10F6:1 40 | 14064116E29F235C7069D746496596D2051:10 41 | 14096FCCD91970F17B52E05099181424DD7:2 42 | 14B1468FF41FFF8363E997946DD4C7DA90B:1 43 | 14FE0CD3D1613C59FA14B97C970CE4C3E84:2 44 | 157FF96F5B3C80E6B77FA604DAD5F8E3954:1 45 | 1595A8D396AC6F7941A84D6F7100B1A7C5C:8 46 | 15A57FD7C945EA9CDC71C31FE3CF796F5D3:2 47 | 161F76D0F13992196417D5B84F2305B5CC1:1 48 | 17E198E4D172250925BD7D89EFDC21C098D:2 49 | 183F5A19D077A3139BDCCB3EA6DA831CC5E:3 50 | 184C1344F8AF3EA61906910510F277E2389:2 51 | 18C8096EE1FA5C8C6B9D3AB9A79D848E266:2 52 | 19BA1F38CE418910C7CB92516DAC6A347B9:3 53 | 19E0061EB9188471E381E9893736CF16EC4:2 54 | 1B9A9E0B079726677FDF4383AA7FFD2C23E:1 55 | 1C12D46C02461550809D10EF62DDEE99F75:2 56 | 1CC93AEF7B58A1B631CB55BF3A3A3750285:3 57 | 1D2DA4053E34E76F6576ED1DA63134B5E2A:2 58 | 1D72CD07550416C216D8AD296BF5C0AE8E0:9 59 | 1E2AAA439972480CEC7F16C795BBB429372:1 60 | 1E3687A61BFCE35F69B7408158101C8E414:1 61 | 1E4C9B93F3F0682250B6CF8331B7EE68FD8:3303003 62 | 20597F5AC10A2F67701B4AD1D3A09F72250:3 63 | 20AEBCE40E55EDA1CE07D175EC293150A7E:1 64 | 20FFB975547F6A33C2882CFF8CE2BC49720:1 65 | 22158C3C153B18E085F0AE99105605AA1F3:3 66 | 2288B6F854BCD5B01DA45F2246939330D04:2 67 | 22ED852E72B423F8D5537C9093C5254C285:3 68 | 237B9E2165C9704F834C9ADAB8B4138967F:2 69 | 2404761C0CDC3FB6A038B7604EAAEE0A991:2 70 | 242C2F9322EBED6AA1A3187B9DAE7EF0FC5:1 71 | 243295E08A856D84FD007A2E602BBD3EFC3:1 72 | 2449F3C910F92602707D9EBD16B81AAAF82:1 73 | 24BFBDC4AC8AD4E530F3F6F991C4887A34A:3 74 | 25AB286C8F4253C862490165D07F56B4419:1 75 | 2648FB0B2EDA4FDFF99BF51E912CD95C023:6927 76 | 26A8FAC5C4B3B5A1D6E0BC262CE986309E0:2 77 | 26F5EBE324C3E6EA1884AF1BE70DA343811:3 78 | 270C60DC07A9247E2770AE4B78870F41275:13 79 | 27C1D736B05CEC7D7EB2D4F604607F8B2CD:2 80 | 28A5154F2F486EA05CD92C6DCCFBF9981C1:2 81 | 28B371AEA0A6A847A0A7A5EAAB8814AD9E0:6 82 | 2945E7A269FAF05C051EF9590B55D862FFB:5 83 | 29767C5C3FB2603E0026976E6CF2656D27D:1 84 | 2A5FC6CE848710F6BFD5A705AF2863435FC:6 85 | 2AC4330373ABE9B9915882537579DA795A7:4 86 | 2B00FB8C3D865D81DA9172F2D9C3BA11CBD:2 87 | 2B76B30EB9C4077D4F227E69644C38F4E62:4 88 | 2C38AE8FF7F40088614359234940931A18F:1 89 | 2DF1A9246DAC8C59CCDD68CC3D45955483A:1 90 | 306A88CC35F16092410A9AF8989EDC10863:2 91 | 315033449A7622547963FC9F5A600402E3C:2 92 | 31A7182C7C20C3418D58342B22E4160700F:1 93 | 31AE86A9651780DB225AAC2D67866A353C9:1 94 | 3290230B7819FB268674CB8D40B75C9A987:3 95 | 32F203CB544F48B0FA79B280B7B7A562442:12 96 | 332D54F56D99A7B6B3D3CFB525826824C50:4 97 | 33D5F8FF2FB3B7F3EAAE8622D1052FA1092:3 98 | 351B76DCD15DA01C646FFBE2FD1307E1A00:1 99 | 3528494E021ABBC644E747F894C53B8BFD8:1 100 | 3546C4E98B01A04F7E93478DF43E845B050:2 101 | 357D0F5152916861E7207ED03AC8DE2AF78:1 102 | 35907AF39307CFEF8F36B2A5C3CC19BFA3C:3 103 | 35A6790F361C7CCDF0F22D37197B1294C9A:1 104 | 36E93A16342E6D0FF79338ACE3DB7BB8ED0:1 105 | 372E6E538CBB3C0DEB2B5A9E8CBD904A7B7:2 106 | 37DA457150458E12A430D61A826D8F488B1:4 107 | 38573F281243B33A5EB0D9F6AD0B1ABC85F:6 108 | 38BCEB8A220BE19DA4C2A5D7596D7FC802B:1 109 | 3A8ADE4CF1DAD5342AF2F9FC9247EC21943:17 110 | 3AE53BDEA0C591BC0B16CA7BA8342378C21:2 111 | 3B1B255A76AC7CC0156BF8681D867D7BDC8:1 112 | 3D77CFFEDDBB08869144C86E0567A70062F:3 113 | 3DD9A9CFC08F04C92E9E18626B8B408CAB1:7 114 | 3EFE0F0F89B5B54AA7FB6A117D25772759F:2 115 | 3F636B2B7A9DA3BE5BCF65248D66BC5FCEB:5 116 | 3FB73FD9C24C0E9DBA7CDFB83DEA139C3AB:2 117 | 40EF60B69F81CF5575FFDA8201EA116B9F2:1 118 | 416507FD35B215DEBB640D5BB3312207E0A:1 119 | 4198A5467A580D9F56A4FA93571E942D28D:3 120 | 41C25879F63E24F0859BADAEEA113576A26:1 121 | 41FA6A87D662364F8E3A96B9D174EE14AA3:6 122 | 42BAADCD710F9EA7E62B60E01D05469AC64:13 123 | 44B2FC5E31A4F4DD5226B3E3F7247891515:2 124 | 454D246498C2B4AE38B9109688A9057050D:1 125 | 461AD70E484C7B36D1C0895239A9453E95F:3 126 | 46549316B8759E8566A31DB4A5DCEF99D25:4 127 | 46DC7CDEAF0857383C43452520619AB5717:2 128 | 47C6BEAA3C33564F963995EF4928317BDF9:1 129 | 48B5B3F11B3FB7F0F81287234EE5B3E2A8F:2 130 | 49463421E41DACB29B74C0086689688A5F6:2 131 | 497E16201345501F10F7B1244E564F6FF64:7 132 | 497FFCF7846D6E50F1C3B631F9E5F52E598:2 133 | 4A58E401767FA161ACC1DBEB1A9082D50B4:1 134 | 4AEE78286DF73B0F3D86D3AB344CD6DB382:7 135 | 4B3E4B6F3249233C26ACB5C995EEC2D905F:4 136 | 4B9B443FDA4DB21A367BCC0ED2D36D2A9E4:2 137 | 4BA08D26E7D66DF501348DB3BBC9850A1AE:1 138 | 4C092CCF57BCF7F0CE752608B57972ACA2F:1 139 | 4C1C5AD486CB1A110736DEDC91A2C064FC5:3 140 | 4C50F4EB9C64756374E97AB89604F12CF29:2 141 | 4C65D9C96E7CBBAB9205636CACFD58CA002:1 142 | 4C98B4FF7CFAA57597E9C57AA370D651A49:5 143 | 4CD9C233C66D766F7DD522BA16AE45C3ADE:6 144 | 4D07954F2231BEF338B718149597F1CC5CB:5 145 | 4DB38ED1F19ED25FBE0F03D00155FFBD2A1:3 146 | 4E2CAB3927DCB359FEA938BD89A130F77EA:2 147 | 4E94E781109936277B5B06CB1A1B19571E9:2 148 | 4ECDE364CBC0AE0B283341DBCAF889DC37B:2 149 | 4F39AFC1819280A809C40C3D3372BE138D5:4 150 | 4F58E0EFC884BB9410B5105682E140A8857:2 151 | 4FA1B9F3C571C20CBEBA1FB5DB03357380E:3 152 | 4FCB0A19A9F4BB445EF9147CBFB39D55A19:1 153 | 4FDDFAEE3A0E987FD60B4458529F0E66B22:4 154 | 50C4D3B98306313FCB2E19B159DFE5089F3:3 155 | 51B0F18F1FB141D15507F06908117FF9E5F:2 156 | 53FF0293B1EFD828D27EFDF55F16F7937DE:2 157 | 5465C464033C00B887595DE51BFE80CA504:1 158 | 546FB8CEC465FBBB4312A3D2F0180694FF0:1 159 | 54C10A810A52F61E095A71F78F8B6995265:2 160 | 54ECB8CB0D2E05F3EFAE12409F9934B134D:2 161 | 55F4F83B9F9AEFCF165130ABA3D03AC5C8C:2 162 | 5649837FB6B77A6636A4FB65D443070F3EA:5 163 | 57A36C2404BD5FAC9350862D7375191B9C5:2 164 | 57D29195CDFD7344EFD11CD4A70CF540F25:3 165 | 58390CEA8FB725FAB7A01B40C52C59029E8:2 166 | 5887B3251745325257533ABB9D2195E311E:1 167 | 58AF3A0231FE86B65D5581E0F81AAED125A:1 168 | 5A13F3216DCE2D2B01A98444E3807B03E7C:1 169 | 5A1684397CF06D1D50435FE71DA92CC9CE5:1 170 | 5A1D0ACB70D3667ABBCCBA6A12A276130DC:1 171 | 5A920A5D5263BDF5AC8E17033D156889ABF:2 172 | 5ABE4E10752E33C9518388610E895274D76:2 173 | 5AFC79A111BDDAB13BFF5B41582731DAD49:1 174 | 5CF118797E15B3A2831436EEC2600D7DD1F:1 175 | 5E2BCB2FEF09257B0306B4744418999611B:15 176 | 5ECA9DCF9DD43458390131ADC7F7B3D30BE:1 177 | 5ED97BB8BC5D44C6C95E4CA5F4338A02DE0:2 178 | 5EFF7FF764DC1DBD43001E889F57218149D:1 179 | 5F0CD063060830BFC6270EA1A9751D656F4:2 180 | 5F5392AA709353B936C6BF31B247E58C1FA:2 181 | 5F6E1F4AEE3D5E2F296E873642C8CFCD77B:1 182 | 5FA4A27FB6FF58C9A32E6525591D1A4053B:1 183 | 608C96CDA8436FB63F1DAA204E8E158A6A1:2 184 | 6364DEE4BB0DD5729C334FE9DCB63637D07:1 185 | 63B0ED16A3959D8C2F232DB10CED4FA7B8F:1 186 | 642CD43FFC28AB87BBF93AA1CAF05B05BD1:2 187 | 65804A014B6430AA2808C2A8D876A1066C9:8 188 | 65ADE0384BF265A3C64B90747F1599A394B:3 189 | 65E47A6E9A64EAC9C3950DA021930BE2B59:2 190 | 65ECCF9A9102E9B9DE7C533CAFC2456331F:2 191 | 660C46EFAF6B66B1C6CA420842416DA606D:1 192 | 6720B9F1290A1B9D5FE8EF4D090A9857829:3 193 | 672E22360F0A39F1C20657F6C578751A651:2 194 | 67D16B114B2DF4CAD0394B3824478E2A26A:2 195 | 681D0CBD255504E3B07ABFC060C480F9886:2 196 | 69E987073909873541D7F546E7E5544200E:2 197 | 69FFB53C7AAE39CB18370BE1B9A43978211:10 198 | 6A16F2C1B70FC38AC528B259B1FAB51D8CC:4 199 | 6A65A90BF869866D8C139780EB10034D322:1 200 | 6AB59FDB74DF600FEC50ACA94E2FB70008D:1 201 | 6B1F1336296EBADAA217C76778DEB7A1D5D:3 202 | 6B2542EC15F7B80889BE89F249C4F1D511B:2 203 | 6B9A3887894D369C8960609A64BC6143EF8:4 204 | 6C89B56D25942AE0AE2AAAEA7496C7C5DFF:8 205 | 6C8D93ECA7476506BD92458DF90C39F6756:2 206 | 6CC0F555341DB4BE31786A9A4F769BD92E4:5 207 | 6D677D26A96B8432A485B38CF5A43A78091:1 208 | 6F03947E951A154A23E774A9A014B39C98A:2 209 | 6FF835580776A26A3D31E7E397E6674400E:1 210 | 703150CF3F56670C2D6376421DF207CC86D:1 211 | 709ED0BDA1678377E6CE8C2816CAC559E52:1 212 | 70C155A82C6EE2BFCA18E569B783B6276FF:2 213 | 70F392C6D8816D8A97970A4E07AB8796D87:5 214 | 710DCA1E8656C8BF2403C940E8DC73C1FA7:4 215 | 71C9C81607AFDF262B33AE2E4BCE7E99426:9 216 | 731A29D65AA9F26AF6378FD5C0E29A01039:1 217 | 7337C3BD5008CBCC81C21FA227B2CEC513B:5 218 | 7393584DA0D509F4D3A241A40C3D4248110:2 219 | 74570315CF1230BAC931552D070AAA7DEFE:2 220 | 7550A5F6BE01B7A448B0BF5171EE5010549:13 221 | 755143A66169EA8D35662468991B728A535:1 222 | 7698261205C18E685A8F92AC30C64105EFB:8 223 | 771956F94167D5D1E08DC6B96D43E087551:6 224 | 78CA709A939372722D02667EDDA6CFBA0F1:3 225 | 7A3EB27B059AB9C0DBE28C04CE17C080120:1 226 | 7B2223BF1662C54DCA023610ED8A4723009:2 227 | 7C6DB6EBCDE90DE0F36F6CC226B098BC77D:2 228 | 7C83C4B584C646E59925A3CC468003AA54C:7 229 | 7CAEFA1DA64A7816CD95CF5C16EB086E9CD:1 230 | 7CCD5B3EB6EB1B9D3EB4AF827CA572D17A6:1 231 | 7D4F0DEA9585E0D1C42ABD2793A9355D875:1 232 | 7D60188BAD089DC1D7BB88012375A72F199:5 233 | 8075CDE2B5981A31E66C8A32BDACC272C8D:1 234 | 80C2504855845C04042F8C2899613DDA434:1 235 | 817EE5C907FC4CB6507BAEFE8ED0CDCE80E:2 236 | 81B000246326E7A45C72244AB023AD3B238:3 237 | 821A21D967813C4DDCE27E48EAD66827945:4 238 | 82E11D09624F96BD7CEFD8474AC82BFF733:1 239 | 82EF3E8DFA456A4C024AF143DB2F6CF620F:11 240 | 8333543F4FF45F5F8F36884A6B93CBED1BD:2 241 | 834BFF53842D2CD0675F42D9AFC0F746B07:2 242 | 835551444C7AFD3414A00AD91C5FC40AC77:2 243 | 83A5679C95CA20A34EC232230A89E6105F7:10 244 | 83DB397CBA6D2413893D6B651343C7881E6:2 245 | 83E1CED811F248A07D1AA95A55C598E7B26:1 246 | 846F3476EDB7F368CB218D79BCE71826CD2:3 247 | 853AE302D08ADDD2DDAB8BD0B2FFE67406E:2 248 | 862544EBFFA510349070B107DA3EE37C95C:1 249 | 87730F8E7D55C99B5FFAD3D6ADD6AEFC242:2 250 | 87A5BA4DB903042A2EF6EAB5A290D9E86F1:1 251 | 893BA737292FC109ED26234EE83CE31C89A:8 252 | 8BAF5C45FB11DF5D3C03F42215317A766C6:6 253 | 8C0266EED59C2791AD7CB27D3AADFAFA1F7:2 254 | 8C08B7E88C86EFD6A36B9918CE9C201EB6C:1 255 | 8C5CB86B66C554B7B46B840898832F8289E:8 256 | 8D2B568A99ACA5E680AA68DCE47A09A86E1:2 257 | 8D30DCA7937A5ECF3B11C17DE41892689B8:1 258 | 8E0D5C9D144BACC76E52C44F5B61E8DF629:182 259 | 8E409CDA87E90A91DFC5BEA3A3751756B04:1 260 | 8EF6868F4EB0607A7CB113C2ACEC08F0558:2 261 | 8F369299AEE0C69A685B903C6E5A212065D:3 262 | 8F54CE81FA3E7715A301979CF01CD559CDD:5 263 | 8F8B8EA87C75228F6A574251207494C359E:2 264 | 8FE581811FA691CADE0AC8E6EA905D24DF0:1 265 | 9102D1F14C10DB9CA75B38ED9A0BB3328ED:1 266 | 911DC08803FEAF1C1C664353510DCD8FB3E:2 267 | 92913B5A1ED91A113239E36AC3DC5319624:1 268 | 92C0C2B1323CC2095774AF6AF91569593CD:1 269 | 93886462AA99D5CA6A883DECB2C1369E68B:2 270 | 93C394542414BB56F99CD97AB401FE42ACB:1 271 | 93ED27A9ED83D48FEF022F084B367F8C16A:1 272 | 941F4DBE019927FFEF89210C3509525DFF0:10 273 | 946DAC1162A7F7EC16ED9603BF0CBDBCA41:1 274 | 948D599B9BB64E76809EDE9FDF3ED9D7014:6 275 | 94EE45F733696C8B7E1F80F2AE3DCB28BA6:3 276 | 959AF1CDA257303073CD6AA805D80173027:2 277 | 9718F656D19667FF839F5F5B82224A153BA:1 278 | 9787F93034022C2A0E8FD9680BF92F272A6:2 279 | 9829411C5B1B6AECAB7020A54D54DAA3E92:6 280 | 98321C82B45C3A4350ECCE813B036664A37:1 281 | 98A2E41029059231B2FE0E2FBF3546ADA1B:3 282 | 98AB3980D86AB254479BAF9DE9C090AB128:2 283 | 98C748A7D7F0A67B51913854E68E2FAEA32:1 284 | 98DC720A8AAFAFB529348F02FFAE491F1F2:1 285 | 991E745F4AB025D5BCC3B833CC45760CC08:1 286 | 99376328AD893CE27992C5C304A54F7733F:2 287 | 9973D2B8212808949BDC1C27B1C30C77DBC:1 288 | 9A64187BCC48B58951D257C52B14FB4BFAA:1 289 | 9B169DAF7CE65D21740C98E86BDBA060394:3 290 | 9B2910F2CFDDD75FFD3F8A66D2A7C94EA4C:2 291 | 9B3D0A0D720CD19E1666D436209FE225A84:2 292 | 9B4810BF7A2AA36C1A69C7BB389C0AB468A:3 293 | 9BDF0C480B0D0A11709B21CBE3817FF543D:1 294 | 9C259745113253B31DD49E1134660E97821:2 295 | 9D782CA5C8B5FAEDE9CB53F6FF59C525A46:4 296 | 9D8FBE84AD481A6A714C8F9F902B6D22602:2 297 | 9D9C70172FC7A76001F60156A015A2FC61A:1 298 | 9E5A1D9A9CC5345A3A211024AA79D5D6EB3:1 299 | 9EBA10FEFD4F0897B93321952A375765133:1 300 | 9EE49ECA6CBCA78605AB8173563F93126AD:3 301 | 9F04970A69486003E6C22ECA0176014DF41:2 302 | 9F15CB4E8267D785B118793E4B3CCE1F617:11 303 | 9F42FC1B5932A6E6523FE05CCA9B9276123:1 304 | 9F7D4B60E60FD8AFCBCCD74041AE3805A92:3 305 | 9F9EE735F041681083BFBCBD1157E1F5DDB:2 306 | A01086E4757714337CCAA928A036FE12026:1 307 | A02902B8D0543C48998493211AEC22FF650:1 308 | A08C645E088E588F9E5B9ABDFCBCD9E9230:9 309 | A09D53E76D02C217CA8FC464E813AE1E5CC:1 310 | A18BF7767F0C617D31FB278C211BD65B454:4 311 | A1AF63850F90EA187A99DDDECAFA7E3365C:3 312 | A209E7253ED38864D64D9780E03FD868127:11 313 | A23F6342DD8EFD8574594C0CF76CB9E8464:3 314 | A2BE597396092AC2D590BCAB3AD9A9DD564:2 315 | A3AB5611237C03DAA93FA05FF59788C6420:2 316 | A469A1756F617687038731E29E65EEE9A0D:11 317 | A49648ABFE19DE8EFA1228DF7B615159625:1 318 | A516C42C8CD4C7E7E328ABB90D002A9890E:25 319 | A5761127EA098A2114C94B2FDB9236FDFDE:1 320 | A590CAE00F5F2CCD9AD5BEC346216167734:4 321 | A7BE1046FB3DF8A326C6D98C1AA8EC625CF:1 322 | A91E69122C7DDDC98E6179943C4B6F27458:2 323 | A947803DC15AF833182B3EA4F7464BD1972:1 324 | AA8A90CA82095CAE59EB9B82CD3C05F9A57:3 325 | AB01CF40155A0F22CC516C48F3EC4C3EE59:4 326 | AB91F322902BFFCEC5C4FC22F2D69761736:8 327 | ABED3B3777EFE04D8C02A044A7FC1FF0EFC:1 328 | AC18F44C596534157414E53436ABC7A911A:1 329 | AC419B146AFAACCA48194237AA29B959DC6:8 330 | AC660D02A9E9C9440B93DBEE477FA2CD523:3 331 | AD750A533273094F74FE3C7C294E2992EED:5 332 | AD87A86518B1462CA197B21409D60560E24:2 333 | AE4E4D5D830167A9BFA45E407A7F513FAAD:1 334 | AECFAC6488926837B3B0290FCA96F0A767B:1 335 | B078561F9279998382B4CE8C9368C3C1A17:3 336 | B0ED6D2DBBE7C5EA643AE067C9A4F5DDA90:1 337 | B2242E00508F38A9AA0A002196E4D2AFB82:4 338 | B28C37A325544401BFBFAFB9E6DD269C7C0:3 339 | B485615DBA7E3E8B56C8F637D4F88B8B309:1 340 | B5145DC5709BC0A8593821805CF79C1A760:2 341 | B51F1511A14B6210C6D589AACE63BA4288C:2 342 | B560FC1B7B6734301F89A6AA3ABF98D144F:7 343 | B5C5880DF9F0C090C21E270D40764E414D1:2 344 | B5CC2B1A08DA02D4B7489827A7F3FF9C223:2 345 | B675D0F02E3B0D944F5E76363CB7C2C46C1:3 346 | B7127FB732DFD38FBB503439374841819BE:2 347 | B87AAFC7DE1821E2335A869D815C455A0A3:3 348 | B8893A5B50CE516E52C501BB5CC71EDFF66:1 349 | B8E4141BCE85C6B5102B075DBDCAE054A78:4 350 | B907A25D1DE622E295A7AA6E645D5813D3A:3 351 | BA6F8B85EDC2133A5073468961A1756FF9A:4 352 | BBB0FDEF3A8EA131FFCC34CEDD360BAB421:4 353 | BC98BA06F816F8C0915440B51D1FCF6BD23:2 354 | BCFAE8A87705EFD6508E3BA356F427128AA:2 355 | BE187C7A53A7B0E95AEB6B68126EE7E3B85:2 356 | BF73319A343C0D4B635C447C9238A5F55EC:1 357 | BFA64A367C50A20CD78BFDFDC2BC52D6F6D:2 358 | C10E35E4531A6C6A1F99E0D6CAC8464344B:1 359 | C17DF61DB70DFD0236DF384CB7DE753700E:1 360 | C2D4DCFEA2C41CB4C5666E72894788822F8:3 361 | C3170676DAB58B394BE8266F51D2B3EC87E:1 362 | C3C305B4B22B5983B779277F177A414DBCA:1 363 | C3E067EE7579FE8F50088945F20FAA8CAA9:7 364 | C44D9395A037CB159D41964D69AD9E4EE3D:5 365 | C7846E3F271F054FB790C30F4504DE65E9C:4 366 | C84782B604BE48977583E108AC6014C5909:11 367 | C933C97C2A353C712DDBAE9506788BD0367:2 368 | C991D5F50650C377CE6D7212E809C8C6086:2 369 | CB53EC83C6D1C81F0DB1013431279D06852:2 370 | CCA584375A77554391530848AC137A42FFC:2 371 | CD2021F110673F8AA95F1C447CAB1DFEAE4:1 372 | CD2B03E36B07B4471FE33A5892E17E0D7DF:6 373 | CD8BE68452C665F7400DE9DAD5485D2F315:1 374 | CDE902213D3FDD1237BF0BE02F05F44A820:2 375 | CF2F87E596758D031C0006D1827C9908E5C:7 376 | CFF6AFD2AB482897C76BCD2D19CACEC3B55:2 377 | D021616E53238BF0DE66516613F1DE72C2F:2 378 | D0E88310DABF9931143593BDB954BFB84FF:5 379 | D107ECC5B8382B3E10F2029C22C01AB1103:4 380 | D1880BD3EAE74BC71EA1D9C550C0EE16DC0:7 381 | D197642F6CD619F04D83487A7EE33D0D81D:1 382 | D39B8E1C477C21BDFF42A0305338E10EE3C:7 383 | D4DF0C13FF004C3CFF18F26296A8162CE6D:4 384 | D5EC2E34EA08AB1F652D4BC9097CD6950D9:2 385 | D767E8EB50640AD167EB99A8B9ECE03311B:3 386 | D78095FF9E98911210386E5B169EAF42D00:2 387 | D780C58333BD88105B3550380BAD608AD00:1 388 | D78252C1708F204B5106FE0C4FE8AEF752F:1 389 | D8014AF60F20C465A6F3BAD7667C94049BC:3 390 | D83C819EE582E3C808BE5FB0ACC03384904:2 391 | D89AD7CC9D5A8E1B0517349AE1B2FD46976:2 392 | D9018B78508091B8FCA7F9BE7E0DAA76C24:3 393 | D9B25E96E6DFD439906BCD824B81D8B9762:1 394 | DA1B4D19AF7EC1E0A20985033EE36065887:2 395 | DA3C904059DBC5DFB0BB1D625B6E842F634:2 396 | DA50E8883AD0D97E2896E81D39067CE616F:1 397 | DA5B71BDF4DCC8694296B26965B824958DD:2 398 | DAA836CF8718E1B7FF44FEB58F75A76AC20:1 399 | DB08FE6509D70B4C0E9553FF66A3DDCA1FE:2 400 | DC9FA3E6AC49AA3FA958E8951BB06240B00:2 401 | DDBAE795D82E39DC56B9F833F4968E5D6AE:4 402 | DE04E96273F101F138AF73E1172F1ED0DA8:1 403 | DE7C435608A4B970752239AC436F4FAF7BB:2 404 | DF2E6BE8296B53F4BF849EF3AD1A80D30DE:1 405 | DF9E6E8A14AE5879867B337758E0A65BC65:1 406 | E0C69CD259D652F5BB3A41BADD1BA540D9C:1 407 | E0F41BF02B958F7DE9C27062905F45AA923:2 408 | E104AFC179613FE49DF005FEC12C419A349:2 409 | E190AC01EA900B96F8BF916D1F53C760C2C:3 410 | E1BD26D42BE2AD9D3BE5A473FB191208C66:1 411 | E235BEDC523A77216ED0EDA7858493533EC:2 412 | E2E4D50DCE1845D79BC5994B14F98CB022E:4 413 | E453123A30E96FB1DF0983C0551D5319C9F:2 414 | E537A2BC4DD5A7080836CF1DE7BC5ECD7C6:1 415 | E5DBEE639FA6E41634F248CFB8EAF548474:3 416 | E5ED4763425FBF2E9C780839A8D5B418970:2 417 | E72C94804E318BD07604F7FAE50B4FB1270:3 418 | E8138597E4299C64074968456AD80FEF505:2 419 | E887391253016D13B30922CEB502457E780:1 420 | E9CD985E2BF8EFC7F005508FF6BF08EE6BD:1 421 | EA2008F79BE2B0E0C02A1642725433BBB2F:15 422 | EA776CD65185506CBD7FDB53544C649D86E:2 423 | EAEB964E915FF8B1E611664084532AF3E6F:1 424 | EB92A3344B40E7EC32D8200F410D0716889:3 425 | ED9038684A8B2E5BE0850F3304A51D5F5F6:2 426 | ED94C46C66D9BDA358030B9B0134DCE28EC:3 427 | EDF4ED33A63B21AD9DBC7130C3702B07537:4 428 | EE0AC0C0AF4A4A712147BCAD882B18D951C:1 429 | EE47E98067FAC07ECBE31ED7090F93C5E86:2 430 | EE5260CE766F47CFC88C958EC3CCD22CBD7:3 431 | EE79A7E71B34F415EADDCF8A6B5BA1DE0CD:4 432 | EE96E27CE6C16BD725DF9D4FD37BC4D2383:1 433 | EEBFAE69E8945473348BA4A8B98B312DB0D:1 434 | EF0E14CCB17E525D76050283148A57828F8:40 435 | EF14C50B9C48EA35CCA0FD7E710C13A83E2:5 436 | EF336D650AE64FCCA4F1CEC8F25AF5786A3:1 437 | EF6A5F7ADA4D07A6696A6AB107A72376CCF:2 438 | EFBCCEE88359A2A2D6743E467532B7E6EA9:5 439 | EFF9F5650357616CDB9D960A70765BF7C95:1 440 | F0A6FC05C68CC3BB7EE1CCA13F2288A35A6:2 441 | F14D95ADD5A136DC694AAB229805F4058C2:2 442 | F17A3DDBB8BF0404FEC1CB216094F6F84DD:7 443 | F294A73A046742292B48C2462CE6A743432:2 444 | F29FDD359957C00F801CC8A1774F703D9EF:1 445 | F32D422E0FC159C6AB218D04ED0FE2AEA8F:5 446 | F33C02F3AA937009C7671989F874A5C69D6:1 447 | F39EF3E98F44B314B60503B2EF0093148EF:2 448 | F4138F5BEDB65BC363EBA6944E5CA752199:2 449 | F60D6384E1488C26DFE31099AD91648D2D8:2 450 | F6E266EAABE91404454B287DBC2B79A7FC0:1 451 | F710E71262A0C34A481186361BE1E454D76:1 452 | F73D11BC6F5AB0B1D2A722E0BE245E74977:1 453 | F78C69EBE2E4522BA28EA12235B3DC4B397:4 454 | F7A5A85FB40A11E700157A9A4C59FA28399:1 455 | F80DCE3BC56F1E7CD5C191B60146A06A735:5 456 | F94031DF36AF0BA745ACA95EA5DDBB34794:4 457 | FA7CD065154FE84F28BED8BBE1F22B5277C:1 458 | FB5F5A995FAAA87A49E8DF65F3E008F3150:1 459 | FBD0A69A6BF3A8EA34FB626F1737492060D:1 460 | FD476CEA93CE558EB387B647444088B7136:2 461 | FD8044E06D204A3BCB1C615E3C7FA73A918:4 462 | FD98E151E6D5BCC6994B7B4A692BB03BB91:2 463 | FE210EA7F291B608505254390849B8609E2:2 464 | FF2CB655CE08DB53D721D10DC1EBE159D1E:2 465 | FF535E8286A9D9394E33ED388B20F73A794:4 466 | FFCDFF228BE98F296C0CA4CE1FC8815A30E:5 -------------------------------------------------------------------------------- /spec/fixtures/37D5B.txt: -------------------------------------------------------------------------------- 1 | 00D36251AFE0A6054D548A2A7769E0F58BE:3 2 | 00F3DDB08F8550B076B2379C25A864E88F7:3 3 | 00F839BB9D1A6D0863E5E7211BFA519822B:2 4 | 017211F18C48BD36F881A7E3E9A90D631F4:3 5 | 017FC12F01C24DF9941BE8D82290B35811D:2 6 | 02079C22F0E21A897AB9457337CA5984C0B:3 7 | 02371C1A67FD403971BAE209BD451303BCD:2 8 | 02671FA61E988501267E93E7122C20E31C3:1 9 | 0406169450E86DEA0A3E4063DA9217D55AE:3 10 | 0456EBD204C8820A71EFD7DDB5CCA0924B6:5 11 | 045B590D91C737E28904092C930806F5962:1 12 | 0499CDB0FCA530423245273DBF17B7F39F9:3 13 | 0535EA03771495E5EE82E4B0018CCEE7E49:1 14 | 0593EAC471DC9D4127DA2083BA20C36394B:2 15 | 06E04A93F78232480F8FD328DF829D58392:1 16 | 0762B737DED01F25B221CF343361449115E:2 17 | 07DB3ACEAB3A75703586FC8918AA6255246:1 18 | 07FDBD7FCAFDFB0EE4E0B3CCD00AE68AC97:3 19 | 0813C716AEEF91B86461D0864FDCEF2CEC5:2 20 | 088265D2BC2086BA43046B1169B873C047B:2 21 | 09A159F251AB22A86BFF846D1128862F148:2 22 | 09EAA73A3A1009336F3235A99F1D8FD1A98:2 23 | 09F19CFBB587CEF7303DDE80E5348FD90E7:3 24 | 0A1F30BB11EC0DB25B0551A496EFEF5F0C8:2 25 | 0AFA0062C25E17951C24A718D3F390C689B:1 26 | 0BA335621B219D0FA58D54E8A3B2B62B018:3 27 | 0BBE00A39676A7D2CEFD3E44FDDD7B76FA1:2 28 | 0C919D942EB5786E6952EB65F92243B2E25:3 29 | 0CFBDE47150A0441D950A04E65C95827C3A:1 30 | 0DAC76F1DBCE7D34B7F14F7F409BCAFC415:5 31 | 0DB89DD96BAE372260CF960A5329D714B9F:4 32 | 0DC222D46538F506438AB35D2B49682CDA8:8 33 | 0E0BA8CA58D5AA702BB7BCB91E7DA7D7A82:2 34 | 0E0D79F66A996A3D4A46937CE0D9E8D9BC2:1 35 | 0EF003F0251D711832AC9AF138ACC326253:61 36 | 0F6609ED84B59E142ECA4BC38C2FF1A581B:23 37 | 0FAA14736CE2D8BE12F1EC3A7A87EACB83D:2 38 | 0FC2F2A52DE9A358C9DF3F2C87E4D1575B0:2 39 | 0FD16B4F8E22449BFECCE1C470A1C80F4B9:1 40 | 0FF6D43B22D049B106A7C91930AE8B3B861:1 41 | 1087356EC211E9D61924F39D1A91E314DCF:2 42 | 11527F1A09623B251C01F5C4D8E0991E2A2:1 43 | 11A10FC517FFA0D4C40972D6E744F255B5F:3 44 | 11A61EEA7C16BDE85B1DBDB49CFC11F7A23:1 45 | 12D7119629745920E23049BB3E8E5506E75:1 46 | 12E69ED6324F9C68CF1C389067F0611659F:21 47 | 13B5F2A9E7CAB27315E337E73A1E74F560E:1 48 | 13F40405192493ECE3657AA2D2A28AF7842:1 49 | 1402EBD7A553D0C91476A0C5E7F31358DF1:5 50 | 1506221CA06F960581E6A2489E073A24C5D:2 51 | 156702578402A2B386F2116D9B64ABB9DAC:1 52 | 156913CB906FC04C6F85A95BD8735B4D9ED:1 53 | 162E59CB6F502C4D7EECE65198007242929:2 54 | 164C2211821132D84D306C63A93E2BE60D9:1 55 | 16D6DF7DA31F018CBCC9CAB27C30492BED4:2 56 | 171541235F7EC058D0B5A23DE6F2395A883:2 57 | 1720CC763B063D9C40A272C0B13C3229024:1 58 | 172579F4EC1D0B91D8C675937398B0A9C5C:2 59 | 173BB6D8AAF2611B01065864719873ADE91:10 60 | 178B56D9297F3222D0EDBC7167CB5BAAAE2:4 61 | 17B3A90D722E26438BC1DADEED6F4608E71:1 62 | 183C2E58877D632B5A6679A7372AFDC8EA0:1 63 | 18A4ACFFC419C19416CA96CF4183090FF40:2 64 | 18B2C996B540F3A1159D78D5458A63436DC:3 65 | 1B184DB4068898EF005FB1F504683AB9725:2 66 | 1B2A8A11F660B0A56A72FB6077D9988836F:1 67 | 1BACF051508740B980BAD165311C870DA1F:4 68 | 1C3B6705746836D7609C50D8C091F302706:2 69 | 1ECA468ED06E450F25DCE76249F352502ED:2 70 | 1F8507C6933AB94A5D4A74595D06212D08C:1 71 | 204255FCABA10ECC257A6800CA61BA93CFE:3 72 | 20C03E145D110223E0F3E4A5ECB269E0944:2 73 | 23242A94A94C0B84BEC245A20592CA39EF9:1 74 | 238169EB3657D483CEC6F35875AAD94DB27:3 75 | 2381EFDAFF58B7DF2AA878B7403A450A83E:2 76 | 238244FDCC29E8EE40AF3C3AF67C0235971:1 77 | 245F7DEB91921CE32ECAA421308508A39AF:1 78 | 24AEADC93DF88B790EFB73BE7546E9AA0DD:1 79 | 25A08A354CFA53F2AA4E43BF57A4B3D2FC9:2 80 | 25CB5911B5F8D9580B04AA361D6406A7A73:1 81 | 26090A24A6C93E0005AB7EEFA6D91B38C54:2 82 | 26876769A7DC81481A428E2F78D86730FE3:2 83 | 26F71FDB04B3A6AF520388820A52B179FB4:28 84 | 26FF6D8485A48B315D93D600E612FE84CA2:1 85 | 27D458F597FCB72B239F4839FEE83A18F62:3 86 | 28080017453FA9E2326C65820B370D20929:2 87 | 281A963823A4112D1D8333EF7DC7B9EF559:1 88 | 28E9AB20540EB19A98660D133E0B590F8EF:2 89 | 2960EE8F2F03AB08098F1F9C8F243A7ABB2:1 90 | 29E0AF9375160F5B1F19FC01CC4B61E62EF:3 91 | 2A210B5B6426CBF36AD51F0B770C52C8B8E:2 92 | 2BB8AFC63B9A49102F61E0848419EDA3982:4 93 | 2C019610E91DB3F5CB1F7FE29BBCAFD7024:5 94 | 2C46B7757DB3C73CD29E568F8B70BBA6F50:4 95 | 2C5F98FA73EE0D2C7FC6A8C921D77852FBB:3 96 | 2C6697DEA32E6E5918970D7881203B15111:1 97 | 2DDED64AAA6CC216A2EF819A60F38DBDC27:60 98 | 2F0F36A11F430516AB81C5290127A199CF2:1 99 | 2F469BB178BAA566624D5AC6FFAD51D236C:13 100 | 2F4738FD3CBEB4879E91FC33B35EEA00C0E:3 101 | 2F7480C14480CBAC973546A9FFB337BC2F7:1 102 | 2FD841BB96C3AB4EBDF46BA233EF5791190:3 103 | 2FE18AB3092FF02ACEE3E4D1926395D8B73:2 104 | 2FF67496238A4B78DD67FAF45CA87C768E3:19 105 | 316C4010324B7F66D1004D376485A7404A4:3 106 | 317D0F1522C03333E000D2551D863D98049:1 107 | 32EB56F578F44FD47DE648E34D0DC276781:3 108 | 3306B3B33DCF8BFD5DF1BEBD26B1ED33740:3 109 | 332DEAB01AF011B39BEBBC8B276671BBA63:3 110 | 335D926BC4198F9E4CD195DE480F9D80970:2 111 | 341DF0C431FF4C3ED1DBC1DB81BC5A070C2:3 112 | 3456C028F961573E8DCCD7F023CD6FE1351:1 113 | 348E6BD96EDD55FA354FC5BEF398E3BC9AA:3 114 | 3636A76988F7C5136B72C13A8B48B6A3E01:2 115 | 366684AEB8F973845FFDBD37CD362FAE3F6:1 116 | 366B9A4137EC95F226DD533C09CE05D8EFE:2 117 | 36DFF0182CB4404FEEF137E594842EB2A7F:10 118 | 36F930D67A99B227EC35992B6E0D1035EE8:1 119 | 37885498F75757DF3E2394D50CDAFE60CA4:4 120 | 379B94B4500E726DE62344B1A6FADF79D87:3 121 | 37E83FCA90839377B82BAB19FD2717F4AFB:146 122 | 382A35C71632FDFA40C5AEA6537D15FF7BA:6 123 | 38E7F986B6F15943C07A5921D558239F708:1 124 | 3966D8CAFADD241BC6CDCB62C949DB1EAC3:1 125 | 3973D3BF4105F3EE2E9F7BB1825C5F27459:2 126 | 3A95BAE5ECC7ED7EAF31B86DC75C5E9408F:3 127 | 3AD0DE030C551BC059BFDAFAFACC9693DB1:1 128 | 3AFE6BFAEFFEA16C2A6C9F33D4B657403DE:1 129 | 3B21FB81F437A6BDCFA1DA2A14BB4877B8F:1 130 | 3B7D3972F3F235BAD154016F8D3C3EC0E26:2 131 | 3C3457FDC05BF88D4D7B41F0F988054882D:1 132 | 3CC63873F4A2B9EC83DC92227DA1118B7F5:1 133 | 3CE1B7534DFC653B81FF876FEDAF3A538BE:1 134 | 3D207AA0C61FE35B59D913165CFB74837AF:2 135 | 3D8DB34E28286A477A0140C30D339A40AEF:2 136 | 3DB265195847E45ADABC438CC45E7A2ACD1:3 137 | 3E2296F7FCD0C921601B3F9C367633D4915:2 138 | 3E93A6C65485A229373DE6B610ADD3A0D7A:2 139 | 3ED86BE0847D2DA12D444C23EA22219468B:2 140 | 402A5224051BA85D21529D9651824273E15:1 141 | 402AD9119C76257AF218904EFD4B502F17A:2 142 | 4061B12AE1AB1FA4080F940FB23DE6E50B9:2 143 | 4294C6C82398F53906118AB696E0C9CF589:1 144 | 42BDD1DA6DC84496298F2B4C6B54BA8D668:1 145 | 42DF725C382492FB97064C918ED7E64C179:3 146 | 431347B315C6F66E8F818D55431C11FE25E:1 147 | 4349BD41B3C71BC0C36EFDEF09A0C478853:5 148 | 43B91715FB146BE7FF67E13D01F94324ABC:2 149 | 441631A145F139D0B6810FFD695485233C7:1 150 | 442793AAC382A843048028744FEB9E907AA:2 151 | 44638AB05623546DEB653BD24318BBA8D6B:6 152 | 462E23AFEA8F9FAC83350823DE3E6532CC0:1 153 | 463CF93E8DBDB892A2002F37FC2E2EF553F:1 154 | 4652AAFE133628FD79778481782B9877B9D:2 155 | 469AF4C6A092CBD8F327F782F3066D0F370:4 156 | 46E533025C0DB297A7902C7F4A3589025F3:1 157 | 46FC0D74BCD84E8B213A7F3233982E55AFB:1 158 | 473625F2F571DF2A4870899A74BAA39D195:1 159 | 47B499105CB2818551B4D9F8997B866CB6C:1 160 | 48484598FDBA0418EBDE3AD156F0A59081E:1 161 | 4879D2DAEE6D9EF2D3682387EC4F1526D0B:4 162 | 48FB8D44D0D309135F5781C8A9F96CADC3A:2 163 | 4901C1B5167152600A515F925A6B47DB780:5 164 | 496D72F7D4E938D832E6431BC1F4B7069F3:1 165 | 4973873776B438774C51322A82110F1DDD1:10 166 | 4A057D96E7BF769B39EA6D9823012E7F000:50 167 | 4A0EEB353591CDD2BC2C4F1E0F03169D09D:2 168 | 4A479A91B5C97EBACEEBA7BC53E7C84A6B9:2 169 | 4AA3E6140D097C224E795E6BD6103414534:2 170 | 4ADD4F70564FF3270FACB190C2B12762E70:2 171 | 4B5D2D19535AF31C7E9F5C76495D28748EF:9 172 | 4B8B0BAF940468C92664700FDAF841FA599:1 173 | 4D83ABD063AA2DDF587E29393615CA16D66:8 174 | 4E2D515AA618BC081110B603DB696BDF8AB:4 175 | 4F3AF38C78D16110C477BB4167C7CF1816E:1 176 | 4F83BF4D4BB98D60E138FFDF5AD7FC7F9E4:12 177 | 502A04987FE0222316F8BEAB1BC2C2398E4:1 178 | 50BF3972945644D781D5333A47389722854:1 179 | 50E74214E123944FF565FBB33E13E90735B:2 180 | 51756BAD142F91CC04035FD8D4C3C042E4F:5 181 | 519FBD3F32858FC76404C7D6DDE285C62E1:12 182 | 51BA8292B18CAC1E47A2DAE18CD8F7302F6:7 183 | 538C7BF4EFA15B62F8F1736A6AB2C65EA89:3 184 | 54D33C993FD5C14CCB098E3ACE5502CAFF3:1 185 | 54DB4DF932916DFA8A7BA9B6F0545CF83CC:3 186 | 5587873742EA25F85C450D0088F840BFC13:2 187 | 559234CB0D2313C08A3D4CD6F8D0BBB48E9:2 188 | 56A711646ED36AE3E469C3464050D99B5D8:7 189 | 56B1BFBB8EB995C1B7C56A56D3CDEC8CD98:3 190 | 57D14ECC18CBC5AA89B58200E326C9EFC6B:1 191 | 57E0965CDC19E4E9B9833852874ACE63C62:3 192 | 58C52B0E203C45FDB433E8A244524C7081D:1 193 | 5953D667339AF32FB5B2B3BEB886B4C5B15:6 194 | 59785BAD7E44A267F7D624899522FF1099A:2 195 | 59AE6F5D0063EFA1DBF54C9D42796EF4729:4 196 | 59DCD3C9E3E228806737BD87C3DCD801C42:2 197 | 59DF0B1544AC70A334CDB8E2600C4357FF5:2 198 | 5A374B03244FBC1DF9A9CA02D7EFB7C2D49:2 199 | 5AED85A0B1C99A679392E9083BF63B6F160:2 200 | 5B1E56F4430C8DEBA2948EE2A685AA7A7AD:25 201 | 5CE606F20D7ED2FE5EDA324971DDF4A9A94:8 202 | 5D1EB552396034C5C0BFC1A4379159E15F2:3 203 | 5DB106D66C6098EB1C39DEDC83EA2C336A9:2 204 | 5E32C8D36EE7202B76E32E4F64F0B898ACB:1 205 | 5ED4489DD825E6A687CAA28DC60372613D4:1 206 | 5FBDAFDFD7A1E291EEC7EE9BE85FCEE3919:9 207 | 60C8A34FDB638F5595AC6BC42AE02ECFDA6:4 208 | 61EA913A52FEFE0C1E61407B99EABB4D6A3:3 209 | 631AA675A5AF9A75E217D4A1DBB956EB10A:2 210 | 63284EA6962ED6AAD59958D36509ECEADF8:6 211 | 632DC95D4A23BB6B7148DF26521A0C65C9D:2 212 | 63593E45C2D041D26FF434F13D8CD39BDAB:1 213 | 63E995A6379A1A11404305CEE71812D3815:2 214 | 6452351EACE2C9FCEC1CFA9FE355613DFBA:2 215 | 64CA9ED7048E2A4F3C1D466FE910AA4C331:1 216 | 6503CD95CB6364942F858FB1A1CEE59405D:3 217 | 651F5C8950E0804E158C526B65185A7A2D4:1 218 | 6555B3D40CD5FD9EEAEA01D5A788868D770:2 219 | 65F28B872C171ACE52945D27E30FBFE8173:5 220 | 665EE21FA1EB30F98F3E89BAE8FF86A4BE0:1 221 | 675553D83FD29B04FB5B7F01BA9D800BE80:1 222 | 6784C22B0E4BFEFF6F1CB8EA3F155CA0773:2 223 | 67D6922EBF23592A4F1B3CBD9E9A20CEF62:2 224 | 6805FBCEC5BA89A673E538ABC61CF44BDCF:4 225 | 68CEF038A668344023B766052F671D078D4:2 226 | 69A3F7BABAA8165F45335EFFAD5B2EFEA12:2 227 | 69C969452F91BCEA3E182BD279E78BC6826:2 228 | 69F041CFE714AAD66BFBEF95ECE8ABC75A7:15 229 | 6B5222F1D09E8F3F5BAA4485AC87EF74F58:1 230 | 6B94EEE2B05D20CBB9E1DDDA7B346A47E51:1 231 | 6CB714A9574C18EDF3263580B1C69F1FC11:1 232 | 6CB9F28C7F15F728904910224680D21AD9D:1 233 | 6D7FF46284CC36AC3E6427F36D7B1A5E56E:2 234 | 6DA0C59F0AFCA33D304F33D5A61F28F6773:2 235 | 6DAA4A59E4167C98E5D64734741723CF97B:3 236 | 6DFE382E404004705757B7F1CC4F07CE2FE:7 237 | 6E102848404B4F42569ED66D889AD8E6811:6 238 | 6E4BBB9B6A5A03E78277354262FB5E22738:2 239 | 6EB071DE2F5A4F0B5DEEADF1003310ED40A:5 240 | 6F52870EA125258BB0CDBF628629719D572:1 241 | 6FDC3431F2AE467A882F7920E7E4C87E4A1:3 242 | 70FC975BFEF7AC6E1B25C853A0D850112D8:1 243 | 7124BF9291FCC7087BDF4404766CB830D14:1 244 | 7156DCC6B34D1848D93BC4858569FAF291D:3 245 | 71D81F942286A70473A5AD3C12A98038B89:2 246 | 71E18D3BE5F5C78D2EA6BDB9837888ABE2B:1 247 | 72B6079C3F5296FA98DCC853ED5E1B7C00F:4 248 | 736C3C37AF2A17D9B93B86A91F70CCA3B3A:1 249 | 74BD4CB5F8C4130BB5987FEAE74D94FFE19:4 250 | 7585688279A8BE9E0F6D61CF4B3DFE176D9:11 251 | 75B478B632AA06F316A3AD5932A81F2A53B:4 252 | 766CA94FD4DE34F830B85132FE2940CE98A:4 253 | 76772FAE6B280B39F82EDFC30738FDC9C05:4 254 | 7678E345392A537F049B5F39303AB4F5DEA:232 255 | 772F3C5B9ABAEDD8FB17FD3EDDFB51FFEE9:2 256 | 7789DC83D4CAB0DD0C9FFF7B2C53FE90C88:7 257 | 77A4645EA93B12B328B649B32AA8E347666:3 258 | 77E46112C6BD1E899650B0983B2E0D5B030:4 259 | 77FFEF092AAE5FE2A4BBE9A8D49E2A34D0B:2 260 | 783930553ABF4D7D3F2D1134C2CF07FF994:1 261 | 789B696622E71A285222BE4A4356D3FA1DE:3 262 | 78C2F7BBF7858E7AFE07FF1C7B29F6A9FC9:2 263 | 78CF28720477FC86ACFF143A3754184C1CF:20 264 | 78FD3C5421520DD1F08C7C2EB77EEF17B06:2 265 | 793598A772F76B57CB603D140AE6DD27CD4:1 266 | 7998932AFB3F44347FC556B694F423A046F:2 267 | 7B5F0E88769BFE3C475F0CF215CBA34D70F:2 268 | 7BBDFB5E636DDB93D704CD8C5C69EF70D32:3 269 | 7BC1E81DDFB5D11C2415ABE54008E0A1E73:2 270 | 7C006C9ADBBBBFF3E70EC5BC39C852D45FA:1 271 | 7C8E71B48D8DEF246C4278DF60568EB5D61:1 272 | 7C9E57DAE2CB908A033EE9898A6BC8EFAEF:5 273 | 7E68DD82D76BF08E8BFC9CFEC4C7CC52F00:3 274 | 7F99C4F31D520694EA66ACEBC007935DB33:2 275 | 7F9EB2E722F10641D5A960FBDB14866D7C8:3 276 | 8040B488128D056CA297D7A291219C55DB1:2 277 | 80BB25EA6BF28828ADA815339D338D5AA31:1 278 | 81469F83B979E3D2D15924BB04998521C39:1 279 | 81A7F0BF9857C1F0EA24994BD8C2E418891:4 280 | 81EAE7C5285380950EE5ADF1C2C74DB70DA:2 281 | 83996FF2AA3976830DC2B97A4390DAFC57A:3 282 | 8467C8C84737DFAAF20709E53178DE4039A:364 283 | 849F3927601B75F48848D50433C9952BB9A:2 284 | 84D1D15420C2417E4B0620C881E7531388C:2 285 | 85A492853AFFD98B611BE7EA758D209AA88:12 286 | 8614277EA012E08DE0EE9E9C28AF2551EA4:21 287 | 886242DF5A6DC1BDBB94AB4B9054E4B53B7:3 288 | 88E585018D4E66DE08593F5691D54090F28:1 289 | 8956203E9BBAA998B91374860AED1A6581A:1 290 | 89FCF98205C2DB45598B3E5C16A24DAEE5A:2 291 | 8C41C026C12273015754AC4304F07550F43:2 292 | 8DEC7BFE2AB8F48DCB7BEDA5C894FC18595:3 293 | 8DF24516C8B85267EFC769A8E1007A5CFF6:1 294 | 8EF8E99DEA44BD8D8BCBC637D5DA17FF838:1 295 | 8F3A7FD833BC8AE7886CFE47FFC266E640B:2 296 | 9051EC589B3F61D678CC22D4D433BFD9D39:3 297 | 90EE75585BAF3E77E35F3D002FED011E3B8:2 298 | 91530FDC7BDCF2801D49BD5F1923CC5AFB7:1 299 | 921A99A995E9408877F1CF85B90C53FAD1B:3 300 | 92C3AA3D15328623DBC653108032F6C1A83:1 301 | 9313A923B8187605991EB198C6870CADE3E:17 302 | 93F80079766B7152C76E16A23C891F346A1:60 303 | 93FBFD18EA4A18D06DA4DD5891B20200F28:2 304 | 948B2BDB3C9B2654CF34872B80BA3C25A1F:3 305 | 9590509BD9B5F93807F92B2E7A02D703B12:1 306 | 95C594F4197D119A448F049AC64750F53E5:1 307 | 95F26FADD4C9908E3414D9124862A388A2E:1 308 | 9634B4E03E8679638718B9CCDCBF2C6FB4C:1 309 | 992B8E299537F491EC115355DAF250BC4EB:1 310 | 9B1B351AF88EA3DE0A64FD0E7853D2EF475:1 311 | 9B838CFD42DFF7847C7AF0EA45E5089CA70:1 312 | 9B99B4478B01166F62DDF509F841C61536B:2 313 | 9BC9910810EB4B0EC2370DDAD8395085C47:4 314 | 9C7FE64B2897E811AADED0E1398DB885FB2:4 315 | 9C814F0AF5B206C70088DB594D747A2D2AC:2 316 | 9CB8E1A6E5814378B9FFBE0BB778B372F1B:15 317 | 9CC7835B4C984F7DCBCD8CD773156F69EA1:7 318 | 9CE85993AAEF3D95B3F9A7E3DFF25A94A98:1 319 | 9D16495E6AF7A0D55374382D86F4C77F598:1 320 | 9E75F53A395AB18BC0449614A1A1917725E:1 321 | 9F0FB10440AB6C2A3F4444EE38C0E0C8553:1 322 | 9FE35159A76E9055BCC4F39A02DAEA81C5D:4 323 | A028DAC6375C897D63E137170ACBA927871:3 324 | A08A7A1EE0129B42370FDC795AE012FF47F:13 325 | A0DA1D4C43493899CFB3DDBD7ADA414624E:1 326 | A1D589C99074289468CD1B62621386A6CBC:15 327 | A27D7217B841A00D9BFFF3C6DAD5A4F14F0:3 328 | A2AED9671D2E2C81EE245F98788D5B5D556:15 329 | A37DA51177A4BE9E9D604E96CBD4118B401:2 330 | A39ABFFFFEB940858A8CB053B209793DA91:8 331 | A43B70C2538964204316BE9323ED17F6A45:1 332 | A523A715B027A5EEE168783CC478F6D5FB9:1 333 | A5EA13A7A02A8313DBC7221B4C66DE2AB71:2 334 | A74EBBD185C6DF4A411DF98FB7773E51F19:2 335 | A7A9FF1548338A4FEB4F0C3DFC0D7596D90:5 336 | A9D47E84CDD4D659596FA3C8A890366ADA7:3 337 | A9E0F21AEBBFE817B567DF9BD0FF2865602:1 338 | AAB5F8811C0069B66138DE6DC05D2ED5BB4:21 339 | AB5893B1E880DE3921B88953195EF749E03:3 340 | AD33EF6E36F3582FC02FBE891B59077ACE8:1 341 | AE1B2CBB50D65EF87C30567C46C54132762:4 342 | AE228701FA0ADF7F5F533463B9C215B7575:4 343 | AE2754D28A88913934EAD8715C83B37DA91:1 344 | AF5132FBEBF5E86FF1EBF54124DB6B44311:1 345 | B03E2468836E81744F488BB9EB32DF048FA:18 346 | B158AB35FFA7BAEC454344F7FD41AF76C7F:2 347 | B1E00D4C2A21BF35239118EC5B793B3CFF9:1 348 | B2DA5D67E63F5D2AFFA5DDB67F03BE500F7:1 349 | B3055A3EF3822A7277F841BD5D0D44C0208:2 350 | B33BEFBB8C939145C1D156A284AB6EEFB01:2 351 | B374E129D684439D6F4F63425A587B67377:2 352 | B3A654BC4F446FA48D9711387C7AFEAC9D8:3 353 | B3FF9A69B09A0425FF0ADDA976709369892:3 354 | B463B787CD6D4CCB2D21B69F00BF17ABAD7:3 355 | B467A7BC4127BAA32A2C8E368A5CB60C32A:2 356 | B4B7824096048D2D1442DB0D13D32B4381D:1 357 | B572651DE552102707C8CE2E7D34AF92D3B:1 358 | B5F14BCD45B34D79A7E8793E31B06D937E4:1 359 | B647D801A8619742ADE5C7C7765BB36D4F0:2 360 | B77D4BE7DA60B618A29899F04BAFAEEAE89:3 361 | B7DBBBAD69DEF2FBCD07A1483095E175DA8:14 362 | B9E89B2B572B0F55329FF7D5A4958CBA08E:1 363 | BB399215B20DD5C16668D64E6843EA8982B:1 364 | BBC879247837142CE9783E0F61792B33AE1:1 365 | BD013A47D48AC9AC9148466A89CDDB8E691:2 366 | BD0C38B81B7E18EFA636D77E3BFD47FE1A3:2 367 | BD49ED5A2EF3C90EBD1D6EB41D0DBB3548C:1 368 | BEAE3FDF8AA8ED83D5BD375F3A11304C32F:1 369 | BF4FEA14B777F017D6CEC88FC33C6115F0F:1 370 | BF6144AD7B2D67B8822EBDFE175F730E912:2 371 | C02E7D512D42BC0F815881E8EF9B7A73BC1:5 372 | C192B5379BD4F023FF3129DAFC8AB02C9A9:2 373 | C21843927B2932173CABEE032EAF43FA000:1 374 | C2725F373E6DA3A9DAAE2440C3F7B7CC893:4 375 | C34A297434DF79192EA38C62767DBBA55FF:1 376 | C4279AE5A5354662A5A71B07A107699C150:1 377 | C474A7271A92496B654C8ED23B38987DF07:2 378 | C54254F08AC1B686879BC004DA4CB764659:3 379 | C55578BB4B601671BAFD660C11E244E9CA5:2 380 | C5826B1C96A54F999D033ABF66654E16698:1 381 | C65B93E1109D57E4F04C39620C177419DC6:2 382 | C6CC15CB49C67E7724D623B30E76BF76274:1 383 | C6EE6C269256BA5A4511E77F923E0684108:2 384 | C760AF863791E9017E5CECACD50417B8AC8:5 385 | C86BFA41C516A50F2E63DCD90FC51D5230A:3 386 | C94C217B54269546BA1710CF7FAECAC95EB:16 387 | CC71E7B23C00153C5B3CC36B31FA67446CA:1 388 | CCF67E3427C55F907112B38B7AF59AC4D47:2 389 | CDBACFA1606653D82C09480350C8725210C:1 390 | CE11AD2B95D1097E6FC534A9F56F4C14186:2 391 | CF7905795F736A43B7D7CE47A17EDA48237:8 392 | CF8AAD5064CE3A9D84FF850677D226B68F5:2 393 | CFDA341B7843A81BE796BD4236F4E811125:2 394 | D05FAB84229F9BED21239294108F8281DFD:1 395 | D0A429930E6CAB1229ED4F779476FF08AE5:2 396 | D268BBFCC63F7507011B007BBD99BE680C2:1 397 | D3BCB71248C891943FC630F9838CCBFFC59:2 398 | D434DFDBFF4F190BC72E4B92519A40D4896:2 399 | D4BF71F3CC808C7DAC157A8C241C8FBD0BE:1 400 | D4C650B6766484A2C353C0DFA672CFA7D5A:4 401 | D502F4EFCBBDBE7A429E60FECEB111F6CF2:2 402 | D505CC32147EF89500B69A187E7A1AA1DB4:3 403 | D650DD77E0C8A128A016F4379BF64E4D796:11 404 | D70CDD190F8FB018688C06CB2178419D729:2 405 | D7D308D058DCFE07887C253D8FD2BD9C1F3:4 406 | D80047B7A1D1AB388B643424792B5715452:1 407 | D88F49D5DA7FE83805EB2F59E68749D73C1:1 408 | D8E7CE01422E61C6F64434889D4DE3B23B0:3 409 | D9B116E32EF911E0DD1C40D12353594257E:2 410 | D9C3ED69E03852E770A042C3D07C725A91B:1 411 | DAA76D26CAA430536994305881ECB7A9AA6:2 412 | DAA7E45710EB572360C35D8637DF1615FE9:2 413 | DB41B3C59C30B1EA35EFC31E4D6583F8467:1 414 | DBB9794AC1B27E83F08AF05E31D72BD8164:1 415 | DC8EFDA5CF83D788E3515BCB24A1900185E:2 416 | DCA821EDB81C8445B7733797D72F2A701DE:2 417 | DCA8E0076EE45E13AE744DF225889F616CD:1 418 | DCCD102149FF9EC3DFF9D358A0D6D83CBCE:3 419 | DD2A534B009062AAF751602FE439820B29A:1 420 | DD4ECB6EF111121A70853DAC482CA2ADF30:1 421 | DD80B484815CE2EE0BA1CC9369B5F067E87:1 422 | DE696CF3E8857289FD489EAEF89DC9988D3:7 423 | DE95662EBF5316A74DEB14A3355B2C43671:2 424 | DE9B25C4D499304A4DDA063CDC08708BDAE:1 425 | DEC93982032843E74A77F96B06169DE88BE:1 426 | E021578B7621B380F36F6237AA21CE9A76A:3 427 | E06BDC85520846E6BEA6AD08E86D601CD53:1 428 | E165A2FBD8C4FFDE09F4D6A680358E765E4:3 429 | E1FD45751F8AFFB74E810D97CE2C199620E:3 430 | E30F79DC1E5EBEBC7FBC8D4145279DF7E8D:2 431 | E3BAB04B478ADCE1E08CD3F25054D51D099:1 432 | E4DADEE6CBDA45E9DF099106FEAA99AA480:1 433 | E558920188CC694E18BE83E9B5EC6A9396C:1 434 | E58E2FCE7DFBBB0A3B35E3E7FE2E0EC4E82:1 435 | E5DB24EDA4014B9D64E96B03E56B5D86705:3 436 | E609C0BEF1A66391E2F8FBD622C2E7B904E:4 437 | E6615E7A6E295769282EFFBA79E8EE11C81:3 438 | E699628E6729FA04C986BF6D68F607746D4:1 439 | E6F1EFFD719B88DB9FD6FE3988F7C610AD0:8 440 | E746F0BC6AB05CB48362C84A9D35B346005:2 441 | E809A432C828A61580BE252B59483D787DD:1 442 | E82FE96F9487CD0745AEC35C653507444D5:21 443 | E8504417DE3576A43149BA5581E5CEAF08D:1 444 | E8C6FA68B0D9EB879EC378DEA6BE90D59C4:5 445 | E8DD8EF9693B4015CAFE3C41BAC9B0B59C9:2 446 | E94955874ABCDDEFD197FD69DEB13C6BC35:1 447 | EA6AC76FEFA36A96471EBD1457EDFA4F71B:2 448 | EB7ED31B7244C247D8AB56A811EE0BEFCD3:1 449 | EB840D9A127732ABCB6EAF905E35ED886DF:1 450 | EBE308DF8A2525BF332EBEBF13AADE35BF1:2 451 | EBEA0B64B04A021CCB622471DD71D116400:3 452 | EDBF5FC9C909042A58331832CB019478FBE:4 453 | EE26D5CA30080BFBDC3A395F69813C8BA48:2 454 | EE6213C5B22D0F1FE495484CFF1CB498275:2 455 | EE8DFED0E021A39AC59B1F40A4161094A90:3 456 | EEC620AE2DA2B22F78942B356694148A100:5 457 | EED9653572DCCC6E566A548D5165CAD3229:6 458 | EF3174FA086C4FEA03AF3A321024804D02B:2 459 | F016EC067A9A70169F3981BE598D18D25E1:4 460 | F0534ACC07D33281DCF7E63CED448DF9A4E:4 461 | F0D2843CEC5BB3E1BBB5EF83FE63BC16128:4 462 | F0D954818976C0A3716DBA270FF70BB412F:2 463 | F140ECAEF1F666E82DE4A79F8B7CE45F0A0:1 464 | F184418A9517F2EC40520BDE69FA53CD7AE:1 465 | F23FCB7AEEF9A5221DEA65DAB3702FC60EC:1 466 | F2A9224B914F4B461BA34571BB01EADD824:1 467 | F312E680090DA221FED07FC11B4D9000D79:5 468 | F349F0C01F71FC2822690854DBE1C37A4C0:1 469 | F3D9FB8DB4D4912C02A6F5F39CD379AF6EA:1 470 | F3F2E681DA7961BBC8FDBE7F32B7F7D2200:1 471 | F459B1C501EDE82C6139DB090C5BDABC6D4:1 472 | F513F4EE52671E65F363770822A2406D0C3:6 473 | F576E9BD0DAB5694E45727BB4554388E7C1:3 474 | F57D96A6FFD0B0D0B2A073F1A47E365001B:1 475 | F5C47AF0FE39EF0527D425DB133A59A1616:1 476 | F5C61B8E121916FCA38F1E1F61E3610AFC2:21 477 | F637C4D44ACDC4B20A0360578B2B43BDC1A:2 478 | F6D1C59E2CAC8D0F9CFE1585B1132AF9C25:10 479 | F7AC40958540F01E761A87E355058C09D91:2 480 | F7F3C17931F539913F4BDFFBEC41BE74101:1 481 | F85772B18770F50D41B66DF242C8913D374:16 482 | F93CE4143E9F8670897A66490AD94B51463:1 483 | F9C95AFA72B62F7CF75CB086752035F6BD0:11 484 | F9FEFDA06A338E7CB4C995F39455BC4E16E:2 485 | FAAAB09A924D19CF659046961501BE4B21D:1 486 | FB13A18750A90B4127DDC1E3FAB9F169911:52 487 | FC3275AF1557E80945E9556FC9243BC3BED:2 488 | FCBF43A7E1EFA7D211A916643C64C1EC90F:2 489 | FCCF8BC52D3BA85E03439E43EECE4245C97:1 490 | FCF61DCC913773319F72DE400E0B00AAB66:1 491 | FD3AAC96AC51E0EC51DBB7B14215FD83CE3:4 492 | FD50E3619C2987C9533A54F16C00D83181B:4 493 | FE35EFEADD3AD6A294C7EF60FEB6306E8C3:2 494 | FE500D6348B384241F9CC466D8A42421433:3 -------------------------------------------------------------------------------- /spec/fixtures/613D1.txt: -------------------------------------------------------------------------------- 1 | 00D234409F9E1CAEE783121B0F2B89757B4:8 2 | 01069C356B65E5870BD248B789A37BAAE1B:1 3 | 01154A4F3C9834B23DFE60BEBEDF86C2927:3 4 | 018917DB3E70493AD1305709DB5A9CF7EDE:6 5 | 03559C25B6DA24663ED0A1B9B2B6FA72C4C:4 6 | 04837D63403CB861EDF94EDE02EBB8C777F:2 7 | 04D543183F5F0621CB9223058C802C169C5:3 8 | 04DBDC12905C6ADBCB7C7DCC81BBAB8346A:2 9 | 050EF646E603CA5AB966FFBC01F4DA6F345:1 10 | 052F382B052E12978D47F2F0574A85E7BB7:2 11 | 05EA5A9C4CA8E01BB9763BD99B07287387D:1 12 | 06459A9C8DC4C5D150052CECAFEA2BA8774:6 13 | 06491545E6D2B41B30EF1D02B351DC074D5:2 14 | 0662E00E5C8F7B1E858FEF5242A24286CEE:2 15 | 068DB9D146A70DBA8F021768A938B155E61:2 16 | 0758C1B4F8A94DEA80C56FF6728E87945EF:3 17 | 078128A948D077619CECE526647A4F3B919:4 18 | 0785717F5777A375374B2A5E9DDCAC3A0D4:2 19 | 08502406ADDE7D83810699FA166E84EBFB8:1 20 | 08ECB8D563EC04DB32CDE3A812EC229C9CD:31 21 | 08FEA6AC850B285BB31672A07B54DA56CB6:1 22 | 09702B4CB157995B393790382B4F4C174F2:2 23 | 0994D72D3D69C4B3A7DA6DCCEF0B654C065:2 24 | 0A0B5FD4BEB590676BB61B0FA7911C5BFB5:1 25 | 0A14E3297720DF9D42CF6924628CE680672:1 26 | 0AA2520CDDB9F35D07A6440BB5F48B9D677:49 27 | 0AA55A4874D8CF647B588305DD77D4E4ECA:1 28 | 0AB0D9768DD8B79DD0873BBDC9402E86615:25 29 | 0AC265966C99ABF6F1277D87BF28A8D189E:1 30 | 0B9926F9C3CF8E23B8E1E493E8908688ECE:2 31 | 0BFFA530E6653B71A3C3C811D5A3B104371:1 32 | 0CF4AC57A599FDE1D1C6C297D03DE362B4C:5 33 | 0D4776F6DE554A7EC820024668BB4B706CA:1 34 | 0D9C0AB5F3E261B199CB1C31BC2D13D192D:2 35 | 0DFEDE38926C684F21EB163D82E0BA44ADB:1 36 | 0FE48A7C110318ECA2C59894C3A5043F802:1 37 | 1165D331A9698C5EC7E0B08C81999B56270:2 38 | 1187C69DED92DCBE6B4A4671599F511633C:2 39 | 12A6C49CF228246FB71DF316B987D604A91:6 40 | 1456EAAE0F2F50F91E502AE92637660031C:1 41 | 14C211AFD9226B2B7DAFA5F219C832625B4:5 42 | 14C7CE9A63C15ADF955BC3AE896E9BB5CA2:2 43 | 14E2FF10A630E63A51547555946BF062A29:2 44 | 15791DDC9B709CF179F0D3C1054D0D121A6:1 45 | 15EFB35517F2B713E578F35A93C06B0BAFF:9 46 | 1621B5081FEA83776BAFC9864F0550B4E4D:5 47 | 16F84BC6A0A6491DDAAC390A33457F127CB:2 48 | 172D840278BF305D033C5E5535011CB3075:2 49 | 1811008C33EE406A7A867C0115E7F45BFD3:5 50 | 18123AC65D45F26EFF33B4AB4464B9D1577:1 51 | 181A4C0103C4E71CA9E19E4C1DE5FB1A44A:2 52 | 18699E121B6669E12D5E52AE51E59CC448A:1 53 | 1871D6A57F78C28B3295B4E5861944139FA:3 54 | 188FBF8D504DC3809139737DA064F647893:2 55 | 19CF7063920F300A2DE665BF7F78CA1A936:1 56 | 1A02E85FB4016C4EE05FFD14FF26D5B3D27:2 57 | 1A1493B2E52B6C2CEC8115C22CD3E98C859:4 58 | 1AB706FCB8D32DF5C8D1721D8692997E4AA:7 59 | 1B3F2D8B2624FA9D27051167779F814CC17:1 60 | 1BD5BC9FA6F1E2AE124BF63E66F90F985F2:3 61 | 1BEAA2507532646DF2C9CD8074D95F74B81:3 62 | 1C9A179A718B52BA6EBB7C3D14176545614:2 63 | 1C9AEE2A35477680CD97981100136E0FC73:25 64 | 1CA416C71370409C3D31C5A6ABD4AB65C9E:1 65 | 1CBD1F9436234F99B934D9B8024E4CA25A0:2 66 | 1CEDD21E68A6CA3178747E154BEF290137C:4 67 | 1DDB8A2DA4998D7A65DB87B5BFBE168D065:2 68 | 1DEC9B6149529F66D3F841E23E1E9C0A907:1 69 | 1E36D54C94E70406ECF68B55C226DC1F6C1:3 70 | 1EBF1D4DCAB97218A09963B2842C4E6BCA4:15 71 | 1ECCE43ACF96E2311FE0CA3022745111863:2 72 | 1EF0C227B20377D158C54DEC859C75D396C:2 73 | 1F3AB01E6EC54F797B7DCC0E262A8961D4F:2 74 | 1FBFBCE21C76E691F942644F931E76D3B97:2 75 | 2070F6AA759F3F25C18CDBCF0C6A8C7F9CE:1 76 | 20B3BE886131184900EF45DB491DC069A77:2 77 | 20B86012EE694A4C3192FC6F0D796CCFF84:2 78 | 214C3B61C6672FAA339F147A2F6C6A3A0A3:1 79 | 21FFC7531660848D2F6EB2FAFD971EDE9B0:1 80 | 227526D54AE040D67005EECE11B487C0DE2:3 81 | 2312C90829EA186FFEA4F97A8D43D3DFCE8:5 82 | 23BB434D36FF838284F064E119D83A93A18:2 83 | 242049C32D35C669FCD031CDE228438A7BB:5 84 | 25164E31D58AE48D7E6E5CA791C008032A7:2 85 | 253C062E8C43B1BDAA570945F3696D8DA3A:2 86 | 268123B6BC5AAAFF0618DBF4EB03BE2F41F:1 87 | 2695178607983CFE3F22A67E0191E8C7492:2 88 | 269763489DE778452E2DB9707C7DEB27B60:2 89 | 2758341928E6E0062662BFC59744608EAAA:2 90 | 27CC0F61E69F55BC096A4C8CBCFAE106E3B:1 91 | 295F00F22FC38F3CC7A7B751A5CB8599734:1 92 | 29EA8927ACACC824689C3068A37780D279E:8 93 | 2AD1BC6C9731292BC8C38D27B174D170C48:2 94 | 2AE2DBA78B468630A589B09F3519516753A:8 95 | 2CB431CBCBB6A987A7F1EE6383CF35522EC:19 96 | 2D227D079A3FB740098A4290E81B7721713:4 97 | 2D51083CD7D406BDA07C9FA22DD95AE001D:2 98 | 2EB7FC5B8CFD70FFB25367B0B41C5E2E7CE:3 99 | 2EC0D3979369F050DEAAA0D0EDC42ED21C8:3 100 | 2EF2251156D2A277C0FA93E1924222862DE:1 101 | 2FD59629A12077F97BA48DBAB4654344F06:2 102 | 30DB7B13A45733D2090474558D3558EBEEC:3 103 | 30DE9A52BDD2E8B9FB1BC48E07E35E37AAF:1 104 | 3114132F3F4EDDC811E9C538CAE361381F9:1 105 | 31D541420CD155DC3730AD106DC265533DF:3 106 | 3234CC45EA552BB1B3D71EABE89A27CA72F:1 107 | 325966A3CE6272CA34454FB0EA271D74A06:5 108 | 32BF0BB23240D7AED3DF80A43C9E51C65DA:4 109 | 3360DD2DC939796043F188971D2289C877E:4 110 | 3377AF29A3F4739159331E303306485CBCD:1 111 | 33D0888EC61E0E5C07CF026DE0BE15F91C7:1 112 | 340EDA1CD2136AC8E967BDEB09E8282EE52:1 113 | 3411AB93082E83247ED703BB1B2FD4471EA:1 114 | 34375999BA494161390EB7C5670F6CD1FC8:7 115 | 34594C1FC0B6FC7FECC7BA5E1D47AD727E8:6 116 | 35092423010677D756DE0C65A10AD1C89A0:6 117 | 3544FF17F83FB1CC6D7226A0427D22F7344:3 118 | 354C785E99DCB4EABF09D2F128959062924:3 119 | 363A88C5A16331FC8A56136D26DBEEA16CA:4 120 | 3681BC577F4A5FB161C4F5EB5130B990746:6 121 | 36E4D274712BA1680A2B982CA7E4F30145B:2 122 | 377E35D090BBC83F619BB603D845230FAC2:1 123 | 37E3FB4B22340EBE4AE42514C818086E3E3:1 124 | 38450F0A3A604AACF1C36964581631FA8C6:1 125 | 38C07D40FAEBEB683BAF459C61118A6C3D7:2 126 | 3907997092430BFC63E1EE1CF4DEBE78E3A:2 127 | 390DEF7556B891C085D4EB6A5E08799EDC6:2 128 | 394C7B4AE0F2FFEA395BCC022B54881B1FD:2 129 | 3989033EE36B2BFD9C70AB8BC53285EF199:1 130 | 39D0E77C91267DA46AD27AE1053532A0DAE:1 131 | 3AEE3A5A1A1AD717FEABB3441A0C5AE214E:1 132 | 3B5CA342CE96AEBD083CB6CB584409FF56D:2 133 | 3B8127A03B9EA22BD4FA8124A08D1B8E250:1 134 | 3C5FF4E077B52EBC498BC768AF043AAD34C:1 135 | 3C9D0F87A4C4C6CAFE42693A4EE1F7B0300:2 136 | 3E62423B65AC39BEAA417B42C9CE50A122D:5 137 | 3E79DBE64754F2031EA0BE56D4643364793:6 138 | 4189A0BD4D58C5E3002162F336AFD7C85C3:3 139 | 419E87A4B572F5D58FCE754040293B8D357:2 140 | 43C20260B767FAEF394C1A6DD2C8B4CCB2C:1 141 | 4468B4E35CFC4A8B6280318D41736DDCEE5:2 142 | 4473FAC1A28F0480587AFD7F2ECFE405864:1 143 | 453E411EF2A692EB18E41693513025B97D5:2 144 | 4574DCB97CAAEBE38DFD6F62EECF6F91E20:1 145 | 457DB30899409D7DFDB03059447D8735C9B:4 146 | 4751734723D49965F454C67BB2E3BD8D2AC:13 147 | 479E993286D050126BB2D0A6E0B4CB7960F:7 148 | 4800A8E2708AA484249EB18993347CD40F2:1 149 | 481642D95C35755109FD8A80E0DFF8E4F9D:2 150 | 4861E3CCCE0E53AB57EFFB45E7855A36B35:2 151 | 486F3D54F209E7842220AA521756618B21E:1 152 | 489C535DA7305ECCC3AFB0C16FA711F66B6:1 153 | 49DE250BEDFDF60ECA44B3DF78E58A6B404:14 154 | 4ABAE2F721931C75BDDF3B47AB289A84F72:2 155 | 4B109B3FD5955500AD43ABDBF70C5D6555C:1 156 | 4B1933BCFFDEEAD4B6E98C5D6A84516A175:1 157 | 4BA40D737F60CA226F454F019E85A8B1BEB:4 158 | 4C0A4A9ADF11FEB96E72B305531C93781BA:1 159 | 4C0E96B0716E68C8CE6EA1CC9BDD95351FE:14 160 | 4C1EE63217258B09B8076FA9029EA3E6A4C:3 161 | 4C72E427B20B38F83158298ABA6102B1FA1:1 162 | 4D44D74842C7CAE2CF66BDB6804FEBFDFFB:2 163 | 4D651B8042D833A8DD781C84DB1A09D122A:1 164 | 4D6E7347522DFD2EED397FD2EC5815F384B:1 165 | 4DA51A23E19983168B8129DEBF42CC6C286:1 166 | 4DB7171CB6738321DDED76F6561EC18095B:2 167 | 4E4D131C825D47C7B9341811EF737F1A2C9:2 168 | 4E6558883F797CB0E7B11A3433660D20513:3 169 | 4E97364874BB7CCAB180B93D0ED5A6DC90E:5 170 | 4EAFCD2851C3FDF3B7E7F405944EBBB1B96:2 171 | 4FC1FA80BAF82FCD80E2DA28DDC02B0B6FA:2 172 | 4FD59EF7B727C468D78919B858AD7374771:3 173 | 5040995BFBEFB06657B57308147E937C3F9:1 174 | 50D481DC0407D2543BBA23A3C3B19E08C36:3 175 | 51689E334D1927BFF27B149581EC21E6549:5 176 | 51B54559411D664F3985FA653FB8A696F2C:2 177 | 5217049D50514FAF33EBC97002338289367:20 178 | 523CFB2FC6686C02F865F15A016C5B41723:3 179 | 52E58E3817F39DC07D043D61886297537FF:3 180 | 53B53973DF64BADC271B8E498E773736843:1 181 | 556A5D534FBF1DAB719107232CBD498B367:2 182 | 566224EFD81ECB3611D7D4517F9D37531F3:19 183 | 56AFA33B9892ACB5EF9DFDECEAEC239DB5C:1 184 | 56B7E3D1A124479CEEC3B21F4D9C2F2B64A:1 185 | 572246C563185944C1981C9B49557DE3C01:2 186 | 57931DF725E433A15832B1988A2ABB47EC2:2 187 | 579E5F5A4C8D9F303957EF49AFB1F80543A:5 188 | 57D0C230033E345D8A82EEB4F456E978A4E:1 189 | 5829990E0FD973F1CBDEDA1C8D62444B9F2:1 190 | 58B2140B2787C804D615A243F506563042A:1 191 | 593A3613063B03AE2CFEAAC75C1D0B8280D:1 192 | 59E6589345DA453276DB6323BB776AFAB8D:3 193 | 5A5A7C91EEBAAD45D04FAE24496B5889895:4 194 | 5A823FE360B9215AC26B630730CEF1577C7:5 195 | 5A8E70E1C8403978D3B45497E7CE9802225:1 196 | 5B376798B951BE6D5C1A376459D0FFFD117:1 197 | 5B551A333CF4C49F299481D75B8B951B2B3:1 198 | 5BE8F8E38149643EAE70252E95F1371B91C:2 199 | 5CCF7ED3BB7CBB65CDCE9CF2AE05D5A87C8:1 200 | 5CF28C86D76BE0F5F88D1492581BA51EC01:1 201 | 5CF386A1C72E03D847256543FF46548B348:4 202 | 5E3241BA5D94DD89E9462A0095859F059D9:3 203 | 5E771269A7473832CC1C6ACA1845E5F676A:2 204 | 5EE9C44DDC5D041D5F04FC6A1F555BFD426:3 205 | 601914AEAE020055905CC8DE73DD9C7ED76:2 206 | 60530584AB71F5C7E130B5A429FCBF0662E:1 207 | 607C4CC0BD647150FF2E2D6DB0FBF58F517:8 208 | 6157D3D38E6E7D424741C802D7F02870726:2 209 | 615AE5FE13AB393C0FFB152689118228036:3 210 | 620884435B9CD27D54C25E1AB53A9FE54BD:5 211 | 6228CD8F36549330A95C23B567AF9A1E128:2 212 | 626D842447CA89701B60F413F1AABF23C8F:1 213 | 6309839C62CE33D115255212FFE2FEEDAAF:1 214 | 6449FF862EA8400EC275F7E6CE32767E671:2 215 | 64CC27DC5B9FBB7BAB4D7F66AEBE3D87E98:2 216 | 64F6B1DF56AB3CB47C32D0505F1C6852685:15 217 | 65282053D3433C9F5AAF83FB492C77EDA36:2 218 | 6536CE089C5B58F4A32DD01833341199FDD:8 219 | 65596A91C4C99FAE87DF31A2574EE831AF7:1 220 | 65C1CE1CE4345E02FEE1A79C2FAB955C95B:1 221 | 66136FD7FC6E9BB9A552D183F30F7BED996:3 222 | 6635F70D81893EC5B4AE5755FB60BEC883F:2 223 | 686FB9A657159EB090326F5589468BE85D9:2 224 | 6A4555B6AAFAC934A6E8CDCF43D39A2D34C:2 225 | 6B222D724E946B14DBAAA9CAF1AF78319A0:31 226 | 6BCD630B0C76FD8B459E71AA59E1CAD35D3:1 227 | 6C4EB61FEBBBECD76A3CA01EC52E179A37E:5 228 | 6D208E1F94322CDB42BEA6C08839F098686:3 229 | 6D3A7D4E37BF97286F7D84518FE55E57393:1 230 | 6DDAFDCB18533F6F7F046924988724A8D10:2 231 | 6DE129A6C39DF50B2A62701B7CCCFB4A47A:2 232 | 6E4AC403259AB01A70D7F5CB00F732AEC4C:4 233 | 6E7982949BFBE7620BDE2D0C826134157C0:2 234 | 6F5BB8F70DD5C993DA9470FF8C489F17377:2 235 | 703B510A0EA23518900349387590C91E2CB:2 236 | 70C160863708E6D1A1F17BD96D56DD400F0:1 237 | 70DE1C1B2A679EB993D8D3814790A5C1AC9:55 238 | 70E57E8A78D788DA611DC7783AFD68CE4D1:16 239 | 718B8CF1CED4D51F080F5BA2CB8D885FB18:3 240 | 72254FA9AEA6A481647C40F3ED2C60BBDC1:8 241 | 73035302229CFF9E68672548C7A730C2713:2 242 | 743E364081DC6299D27055FE21A84B110E4:2 243 | 7518C18C3130C9ED13160B2CC12623E02E5:5 244 | 7545BCD88C9BBD9C5576ADACF9F6566C8B1:3 245 | 756550E4507AA92701A9EBE0B2AA87E6013:3 246 | 7569A6705C48554A98F4DE3F7A9CA603227:2 247 | 75764E1CB6EFB8B64E189CC607DAC6EA68A:5 248 | 757887241D5AA8AF97CC6267A9AB0A81123:1 249 | 75F08A8F01F847A30CB93BB62DF8E6B3719:3 250 | 77324610D9BED913705D9E6F87C9614B27D:3 251 | 7735C98B92EBB943EC33F408812FE02B0F1:2 252 | 77C1E7E95F5BE913CE700A522664BF8DE4B:1 253 | 787312A42FB018234A3720EF9A09EAC1958:18 254 | 7904C53278967D10893BCD7DABEE074B422:4 255 | 79063F74C934538AF5FAD6AEB4082424EE1:1 256 | 798D32DD291AAC7246C7DEBEC1A6744677C:3 257 | 79CD5FCAE232EA24004D445FCA54FCCCBE5:2 258 | 7A344B97C445802A608FFEFFC3B5C5C7A41:4 259 | 7A673376AD6E95AFAFF27E9497E70E63DF4:2 260 | 7BACE9EFC91D4E7A1EBE928D2F32D7BFE7E:1 261 | 7BD788E3D4940AEB09E08ABADAB5AB20949:1 262 | 7BF58A6780E1CDDA759741BBF991D7A2614:3 263 | 7BF84A63608DB99B19C9965A54CA2A19CFA:10 264 | 7CF92C4632FCDE2D40D429172370FC43182:2 265 | 7E80E8ADBD8D80962CB8415970079BF910A:1 266 | 7E9F86B2A8B33F482BC2EF9501CFB28E9A0:2 267 | 7F6399205B85AD7374D6155DCDC2DAA8FDB:103 268 | 7FDD134067FA094917A8A9A2180B8A65D3A:4 269 | 821285708E50177917CF0C52E528B76354A:2 270 | 823B5CBE9DCB9501EB7FD31769087767028:6 271 | 83FB6AF97B81E5B8DFDCB45A27CBEC6F50C:16 272 | 84EFF10197DC340026847BFE42AD39EFD14:1 273 | 85B18C69DA40C3D028E90E685B71F98801D:2 274 | 85CFA8C82685588D0AD710ACABDFD843131:2 275 | 85E7F594C4CF567F7AC050C719A9B2C1900:2 276 | 866521C245225AF3D4D349D229C76EB5055:2 277 | 86E9017FF519759F52F9F6C856D26B2740F:9 278 | 8717A7311F43A4FC21826BC54132CE78F49:53 279 | 8727AEB2A0493E3059FD4B22E78C60A7A50:2 280 | 873645F3D0B4167D206A49941B2B03DE2B6:2 281 | 8862B3537B559F7199F74AC2BEAB5EDFC08:2 282 | 889D3A2C85DFAED823FEBE99D3F2336F0E6:1 283 | 88A2921C8A78C96F61E19ED494153E8B129:6 284 | 88E2C8CD496848C51EBE1A4CBCB8F36BAC1:1 285 | 897CE45892974B0B8D01C480531B99E5D6D:1 286 | 89EBB56FB2F6A99CEEA71A3FAD2587957D6:1 287 | 8C4F67E4E5CAC35AB87F8CA53E022AB17FF:1 288 | 8C8474F903D293D763DFE7967942D354AEC:3 289 | 8D3D17AFD33B9570A0BA1ABC78768C13533:3 290 | 8DB9DCD09A23C55C975F08D8E548128E9C5:1 291 | 8DEA31DCD6F4D898BD24035435633FDEF54:2 292 | 8E83F900A48F2F234B588101AEB4E10AEA3:5 293 | 8EB994C5CD5C338B65E0523DE3DC44EC242:165 294 | 8F2208F22D0B06D05B3A463399A65234616:1 295 | 9075C025CCD0BD4658088A8DE1805EFA7CD:1 296 | 917DA19E037C10817BE8F34FF3DCBD1EC44:1 297 | 9257691CED8D90F27373AAD7A4CFD795CBE:1 298 | 92A96AF55EB1FD6B0343D9335F7CB00E365:5 299 | 9306A87716BE991B355AE8EC26CAF1A4080:2 300 | 9365769A9AF60B92E08194250B31F217A65:2 301 | 94D0467B4B7DD63A4E25DEC5A8DC6F2A4B3:2 302 | 94FE150DE7FD9EB40026C5CB0EDE4F939F5:1 303 | 95350BF80D3310B154BCE2345D9EB0ED314:1 304 | 953580CE8A46F77830EEFA47A3973353F7E:2 305 | 95518BFE1B4D03F44382B287CCE81C440C8:1 306 | 959E31E9FD1E2E1B0AEFDD5B443196E8F89:4 307 | 95A9EB5AF697103D2CEB647D90AFF409D6A:4 308 | 9664AE116395CDFF0AF16A49CAE375B9C78:14 309 | 96CF24A31D86815B331273491C761BB0B99:2 310 | 97134958EE5682DF9F5190E041429CC55B0:3 311 | 9751335474D623B8370BB7623847E1EEDEE:2 312 | 97A3AFF97D28945D617BC2124913575DB1D:31 313 | 97EFA4D202527E8D164144A55A919D242CE:1 314 | 98FB0C6F9F121579184A819C671609F989C:5 315 | 98FF379CC9DBDFB934E45FECFABC2902FE8:2 316 | 9A328D3BE1791D4EEFA4621839001F2F17D:2 317 | 9A76AE9B02AC6FC6CB3090D740B27E3A790:2 318 | 9AC4C6F97DC60CFBAE04DCBABEF3B8AA26A:4 319 | 9AD339E6BE76273A522519208285C3AA3B9:1 320 | 9AD7A036B3F8AED2BE75BAE8CB25698DDB0:5 321 | 9B0988851BE2F970C3CCDD37378DF38E7C5:2 322 | 9B0C47ECA6E76431CE85AC084B8E802E07C:2 323 | 9B2CE1FEF1C6D659DF3EEA2A1B368BB042F:1 324 | 9C3FE7A4A7850287CE15E80C697DAF9A897:2 325 | 9C4C3F80C3C6C96ED6F7BE2C0CC96602A29:2 326 | 9DF64081E7FAEC236628C080A1EF2F10A99:2 327 | 9EC9E12042409689D0AFBF548C5B84FEA43:4 328 | 9F0BA38A5B6FB74466730A146EF59A27E60:2 329 | 9F98993D9C5CA040734359FE5C75A4AAB68:1 330 | A02FF641D4B30DB0896C1C3FA7F6A7266D0:2 331 | A0520197F8C4032B87A34A2FE5D4A1A1AF2:3 332 | A0D976196D21D59FB4FA2BA4F8E8D2D0DC0:1 333 | A12F4ED05D0577F481D46426C8D9C5D0EEF:3 334 | A1411E3C7DDE1C9DA68AC8C5345F35D6882:2 335 | A17E3DFFA49C0E15107D736654DADF41EEA:3 336 | A1A6C4331C45B23A7EFF0FC7B717AD78309:2 337 | A1C4F7D39B5FE5C9B3A75344DD31CF6EC61:10 338 | A20DE07BE5AE986D2CE7A7C8DC1B74B8C3F:2 339 | A26751B111E5C6D7AB73D36A97FC7C4EC8E:7 340 | A2929B8B7993AEE12A9F4917BA3B61FD39A:2 341 | A384FA30B2B47BFB3E0CB30F8D9766A655F:9 342 | A3F4A5444B7EA84C06DC25962661BB1085E:1 343 | A54BACCAB8A9051F1BB12547A117E8FF51B:2 344 | A600B479C82E20554A74F878854B91E1611:2 345 | A61143F0CA3113923E1E31D87FC45B9BC6D:2 346 | A6A2C94203345D76DCFBAD51EBA61040BB6:1 347 | A8B2F6E2A6DBF98727E040E30493FA2B77C:3 348 | A9A45B877BFABF4ADD1DDE8361DEFD73D23:2 349 | A9F4E4E731E607311FE8769798CBDC15605:2 350 | AAE4755ED95B199E53733AB5693FB33366D:1 351 | AAF01AA4B0F21D14E0F28338C1BA568C33F:1 352 | AB3FA98D88893E0D0EFD52AC8E02615F838:5 353 | ABACA7BF04018122D907F88775116CA79A4:1 354 | ABF54E91E0CBC8354AA1D54B4892CD9A825:1 355 | AC348C063DA4C60A947AF3B599D5F24C198:23 356 | ACFC5D07A29DAD53ADF7C4346D516176B8A:2 357 | AD517AE2A4AC45E233CE00990A847FA9374:1 358 | AE319707FBB96375BBD8FC5DDC61F68FE73:3 359 | AE6B8549ADC16CDFBF2FE5BEE7E6B9BD6FE:1 360 | AEF4C7C3BD86989BBC752DED4D4871EE4A1:2 361 | AF2CBCB8E3950B02671792ADBBE121B7BC8:3 362 | B012BB16051F3A02636CAFFFD8E794498EB:3 363 | B0CB179F35A47995BE22F6F2B3646E1533F:3 364 | B0FEA1E7DFBF6E2F4A1FFEE9BE4941FDE9B:2 365 | B13C9FF531BE591AAF331B1F2D9365BFCBA:4 366 | B199DFFEC56F45CF5AA4CD727577F3FA21B:9 367 | B1FF960D77BF3B0918A37A3839AD48C17BD:1 368 | B2C011EBE8B20C0F386967B8D7C1FA17350:6 369 | B2DA13698DCC0ABE2F0822EC80F1731FC64:2 370 | B3301610934ADF5FBD7E57E4C1D72D2EF2D:5 371 | B422BF12971D9E75B00CAEC274544F01336:1 372 | B439A27B2A716832F2D588EC8878BECCC8F:4 373 | B46036B76EB6001CE47A9F82385C21C0EA4:2 374 | B55BAF4BCA665FA169D4E879B136778C192:2 375 | B607A132F04F26EEC9D069F94014EE896BD:3 376 | B613E22489CFA3E25A6FA2E89FC993A7B1A:1 377 | B61BB4B8462C800839A60E003C9BD4F21A1:2 378 | B6461988CEBCC747931D9B8E7D32F5E33EA:2 379 | B7BAB5830B8DC428F34D6E0F5E2AFFF5DA6:1 380 | B8137AC97F2F734322513D8161B59318A62:1 381 | B8402338651127D34E34364D294A9A035F7:6 382 | B8515927016CDBB3599F76AF4DD4E75C69B:2 383 | B854F0934DCB6C7861DE2E146C148405534:2 384 | B9937AC1EFA4E5A8823BEA0E550673D6ABA:18 385 | B9DBD4CDD47DB6A80E47A85ACDA7BD2BD64:1 386 | BABC1982334369F99A52D1B7B011B47969F:1 387 | BBBC21B4946E638D1A785075F07DE7BFE8E:2 388 | BC065295270AB7D53A3B788B3CB78182703:1 389 | BCA726013860836DAF4A1289DB22AEABAD6:1 390 | BCC9A44F7448F80D2E3380DE114DE4B7ACD:2 391 | BCCBFAFD4730DE73EED3D9C3B97057AD268:3 392 | BCCE1C340072F52542A2F92841ADB7A368E:4 393 | BF5719C4D5F102BC6414A2C40047D2EDC27:1 394 | BF982FB172F4D27E250F2ECFCF7ED495FD1:1 395 | BFDD4B2A85F9C6EDDB9EAB6F63737CC7939:3 396 | C001870C27F90EF78F48ADBE977F2497F56:1 397 | C01387481E9EFA4C4C8E66A316603F6C44D:1 398 | C0F79FAED04FB45C8DFE8404C51369C3D9E:5 399 | C1A8E49F0F9D1A9D5E70BE3784FE600C477:22 400 | C247F4CCDA97747DBE7F7B5FC37A78FEBA0:1 401 | C2B0E63DA2CF6666882E09B03A36636D5D3:1 402 | C2C2AA9DF7A5CD9439B9BF2194C18743DF0:3 403 | C2DA699D8965CF95F28AA7ADDF9DFAE916B:3 404 | C2E1C41225B5458072D74AF0724F74425E5:1 405 | C34014FFEF219C813B72DF32BC59C9301A9:1 406 | C38990A61D2D051B40153834BFE5CAE3091:3 407 | C39676DF71BCD993A4F6E22D8AF3A2F2903:8 408 | C41CC7B191167C7F13DF81BD9960BAAA27C:1 409 | C4CBCBAAD282382B5E02DF56D126052485D:17 410 | C57F497F9E5E162D23EDE9497AE000782C4:1 411 | C58D7E318310CFFA777F7B0AFA4F27B0044:1 412 | C623A3AF76D6B3803C06639766901BDB8CF:2 413 | C71015582C3A00D7BF0CD1C621294E3FC8C:2 414 | C714825C12C00B2C2988FDB41201C07D045:1 415 | C74768BCE89A5AB63F10BE3E76321A3BE7E:1 416 | C7826044171DBDE8E31E512FD7A492309DC:2 417 | C79D05A704CD65A1B06F5DE27DCB55EDA37:4 418 | C92BDFF98C5A293D520B58556AE9071759B:1 419 | C9FCB8A6DA7656D585C8E7DE90D98E72ED8:2 420 | CB0CBEC77D5EE0C73A3E091C865DDF1E11B:1 421 | CBDF55C88F86E677B461C6D1AED9743BA39:1 422 | CC37B0159BD5096A43B4DAE1B102355004C:1 423 | CDB0C9B6C951BB43CFC80529BCD8C2A34CF:3 424 | CDD53FA1DE93134A1DFFADCC751A2ADCFE4:6 425 | CEEC4BDB12977A512416DCA5BFB432CAA2F:2 426 | CF39D47C7D6867E0E1F60C2CC49C98ACDA5:2 427 | CFA0B482B7F0F80061616326D09679E815B:2 428 | D0CDD338871CA8480035CF2231976EC5A9E:1 429 | D171F6719A7C57BD7F7A74CC20714DC58EA:3 430 | D22FD6EB7F35D303CE0DFFC0D54E84AF189:1 431 | D2324124D5503E26D7EB97CDB237BDB4BDE:2 432 | D2D14D1C40E2DA81F8C00B78A7ED84538BC:2 433 | D2E18216339C3F7787A3B98337D5E2FF7D5:2 434 | D304B6BB7C19C6FCF1A16734102705696A6:1 435 | D3270E8DFB1935F8CDCD9BE19366C8284E9:2 436 | D35D3F91868308BE291957691C77CD335A6:3 437 | D441B5805CB7796E2EDAA494C063FB8037A:2 438 | D49557F82843AC8DC89404F67EDC986CD5F:2 439 | D528AB48BCF6C69654BD2540C3CB60256EB:1 440 | D75A0ACC7950AC485501D83294B8BA34481:3 441 | D8AB71C43D74C0BAE05A0284C013CD6CE72:1 442 | D8DF6F89F1566D0AD59C5AD395DD1C22870:3 443 | DA7BB5FC437A8015ED796FD47449E59DC30:1 444 | DA87B38DDF699AE303451C1D963B29D4B6E:1 445 | DB66673F051A483BC5F70915E4253D8CB5C:1 446 | DC84961203185FB4F15C63D4E82F8872F81:4 447 | DE11E8D832D113CEAA69728EC8E9B38C4E1:2 448 | DEDA67F9D482CCB829526E82A03D9CA8FB5:1 449 | DF14FF6F5A9C7E4BC60417DE6B327B66BA0:1 450 | DF529192A6A98AC8E120C2511F210FA28B1:4 451 | E0E5C941B0E8E8FB688C149B8C9CF765E0C:2 452 | E133E466F5E3F4B1C6EF806124C9F92415D:2 453 | E3528B28109DD93E0D8A517D0D35EF69593:1 454 | E3807D0AA4C3B74159D41E2B6F1335D8F9C:1 455 | E3B0FB45F82AEFF0AC66DDE1C90BBE028F8:1 456 | E3CCF329B9AE5B02F9F6C36592924315C19:2 457 | E428B5C3E1ADD829E8F83A5BC5F99DECB63:2 458 | E47A6A05DB6E31A05143D729A8CE2ACE58D:2 459 | E4897F69050180A1A74BD2C76C3CFC21D97:1 460 | E4B28A9013720AF107761D1C4CEC6213FC0:1 461 | E4DD8CD1D9BB984397BE54296D00427B3EB:4 462 | E4DE9C8AB3561E62726D142CA58718DD461:2 463 | E57A678D157BAD80E29677A2D866022007A:1 464 | E613A29747CF6471F70B10AA2A2EBB5DD42:2 465 | E630AD5BF05F68A900D244563C96C29610B:2 466 | E6354E4EB96B37503FD234E6F0DF538FF73:2 467 | E6480515F3AAFEA070A951417680AF196B6:5 468 | E6970EB0445C52609A7D2B780C02EE8C760:4 469 | E6F45BB33AFA293C861173BA0611611D19D:3 470 | E74C18F7F2314635E75922AFFC97E29110A:4 471 | E74EF3B1940DBB3358E678E346A3BB8BE04:4 472 | E81775934A6B714729BEB79A85642FF26E7:1 473 | E821DCBDEA82D1D602E99AC49CCF4135045:1 474 | E84E21E5CCDA2127B144AAD42788822F214:2 475 | E95ED0546988039CD9BC5D55BD40D9F975D:5 476 | EAAC08DB07AA30B7DAD289A06912312463D:4 477 | EAB37A3F0E47640EBDCE5C648862160212E:2 478 | EC5C7CBFF11C4DE75C0CD0D0A3DA74F4902:1 479 | ED73FDB93BEB07F23C813AFE9FAB0FDC882:2 480 | EDB951883A1DA2C7E919463E8D5D7775EFF:18 481 | F01AC4FD14DC11686DDC54016DD9A5F8E91:2 482 | F06EFACC01F09FE0994D9C8D2381D0CB786:1 483 | F101F644BA422835CABB3146EBF97902C27:1 484 | F13B2640DF5B59397224480B45DC9C4FA19:47 485 | F16CF0CED9C924D142BF47DB4BFE607EF4A:2 486 | F18DBEFBA9F1345507806F5612A6AEED534:2 487 | F2A175E69231ACCAC5DC370CBF3BDDF57C5:1 488 | F3AF125049A36D597241FED64D54194E40A:1 489 | F3B76DFA1CFCF66696ED98CCE6C871FE9FE:4 490 | F3D6084EF1ECC2CAAA35707AA8C8C69E221:58 491 | F48DBC00E430B23710AB259A18B4E3E35A9:1 492 | F5B5C75A4308550CE1E8CCC2E81DC027BCB:1 493 | F5BD8C4A0F88B252423B0EC9C73DF3E6E4B:2 494 | F6059B19FFB2941A945BC7D34DBB851A79A:1 495 | F6D7240DF918DCD08248C4DE2F1FB58037D:4 496 | F7409C1AF16BE69CACD480E74D4A1A6427E:3 497 | F7DE8DED0CE11F1EBBA4069E1208205506E:2 498 | F94D90B7CC7EAD7AFA8D3199ED789A0C43D:3 499 | F984E3EF66E8EB98512C3D526CDDA486E78:3 500 | F9BC54E9A1F7C66B71D722C128C181F17C6:1 501 | FA523BDCA0F5FE4327E2FB3EDCBA0E0CF44:2 502 | FC527A51138A6F46D86C94D432693D36D52:1 503 | FCAFD901502A1CFC8991FB43B6FD115BF9C:4 504 | FCC044845CE35CFC0D61AC8FE2CFA9A0FE1:229 505 | FD48CBD4AECEA9F1DCBADEDB0A4677D3710:1 506 | FD59B45B4C9A583023B68EAEA0678A9FB1D:4 507 | FDDCA33121A05180DEC8415CA74C4DA59C2:2 508 | FF9A76DC0291F6095371BC20FCC8030A46D:5 --------------------------------------------------------------------------------