diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3389e52 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,66 @@ +name: Publish gem to rubygems.org + +on: + push: + tags: + - 'v*' + +permissions: + contents: read + +jobs: + push: + if: github.repository == 'bcrypt-ruby/bcrypt-ruby' + runs-on: ubuntu-latest + + environment: + name: rubygems.org + url: https://rubygems.org/gems/bcrypt + + permissions: + contents: write + id-token: write + + strategy: + matrix: + ruby: ["ruby", "jruby"] + + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + + - uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + + # https://github.com/rubygems/rubygems/issues/5882 + - name: Install dependencies and build for JRuby + run: | + sudo apt install default-jdk maven + gem update --system + gem install ruby-maven rake-compiler --no-document + if: matrix.ruby == 'jruby' + + - name: Install dependencies + run: bundle install --jobs 4 --retry 3 + + - name: Compile on JRuby + run: | + rake compile + if: matrix.ruby == 'jruby' + + - name: Publish to RubyGems + uses: rubygems/release-gem@v1 + + - name: Create GitHub release + run: | + tag_name="$(git describe --tags --abbrev=0)" + gh release create "${tag_name}" --verify-tag --generate-notes + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + if: matrix.ruby != 'jruby' diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml new file mode 100644 index 0000000..176faa0 --- /dev/null +++ b/.github/workflows/ruby.yml @@ -0,0 +1,56 @@ +name: Test Suite + +# Run against all commits and pull requests. +on: [ push, pull_request ] + +env: + JAVA_OPTS: '-Xms60M -Xmx1G' + +jobs: + ruby-versions: + uses: ruby/actions/.github/workflows/ruby_versions.yml@master + with: + engine: cruby + min_version: 3.1 + + test: + needs: ruby-versions + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} + os: [ ubuntu-latest, macos-latest, windows-latest ] + include: + - { os: windows-latest, ruby: jruby-head } + - { os: macos-latest, ruby: jruby-head } + - { os: ubuntu-latest, ruby: jruby-head } + - { os: windows-latest, ruby: ucrt } + - { os: windows-latest, ruby: mingw } + - { os: macos-latest, ruby: truffleruby } + - { os: ubuntu-latest, ruby: truffleruby } + - { os: macos-latest, ruby: truffleruby-head } + - { os: ubuntu-latest, ruby: truffleruby-head } + + steps: + - uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby-pkgs@v1 + with: + ruby-version: ${{ matrix.ruby }} + apt-get: "haveged libyaml-dev" + brew: libyaml + vcpkg: libyaml + + - name: Set JRuby ENV vars + run: | + echo 'JAVA_OPTS=-Xmx1g' >> $GITHUB_ENV + if: ${{ ! startsWith(matrix.ruby, 'jruby') }} + + - name: Install dependencies + run: bundle install --jobs 1 + + - name: Run tests + run: bundle exec rake + continue-on-error: ${{ matrix.ruby == 'jruby-head' }} diff --git a/.gitignore b/.gitignore index 3c40c7f..633ad7c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ tmp *.jar .DS_Store .rbenv-gemsets +Gemfile.lock diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 712c07f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: ruby -before_install: - - "echo 'gem: --no-rdoc --no-ri' > ~/.gemrc" -rvm: - - 2.0 - - 2.1 - - 2.2 - - 2.3 - - 2.4 - - 2.5 - - 2.6 - - 2.7 - - ruby-head - - jruby-head - - rbx-3 -matrix: - allow_failures: - - rvm: ruby-head - - rvm: jruby-head - - rvm: rbx-3 - fast_finish: true -script: bundle exec rake diff --git a/CHANGELOG b/CHANGELOG index 6c2b465..a2c9982 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,26 @@ +3.1.22 Mar 18 2026 + - [CVE-2026-33306] Fix integer overflow in Java extension + +3.1.21 Dec 31 2025 + - Use constant time comparisons + - Mark as Ractor safe + +3.1.20 Nov 17 2023 + - Limit packaged files -- decrease gem filesize by ~28% [GH #272 by @pusewicz] + +3.1.19 June 22 2023 + - Deprecate passing the third argument to `BCrypt::Engine.hash_secret` [GH #207 by @sergey-alekseev] + - Add GC guards so the C compiler won't optimize out references [GH #270] + +3.1.18 May 16 2022 + - Unlock GVL when calculating hashes and salts [GH #260] + - Fix compilation warnings in `ext/mri/bcrypt_ext.c` [GH #261] + +3.1.17 Mar 14 2022 +- Fix regex in validators to use \A and \z instead of ^ and $ [GH #121] +- Truncate secrets greater than 72 bytes in hash_secret [GH #255] +- Assorted test and doc improvements + 3.1.16 Sep 3 2020 - Fix compilation on FreeBSD. [GH #234] @@ -16,7 +39,7 @@ 3.1.12 May 16 2018 - Add support for Ruby 2.3, 2.4, and 2.5 in compiled Windows binaries - - Fix compatibility with libxcrypt [GH #164 by @besser82] + - Fix compatibility with libxcrypt - Fixes hash errors in Fedora 28 and Ubuntu 20 [GH #164 by @besser82] 3.1.11 Mar 06 2016 - Add support for Ruby 2.2 in compiled Windows binaries diff --git a/README.md b/README.md index 9bbc739..88598ad 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,9 @@ An easy way to keep your users' passwords secure. -* https://github.com/codahale/bcrypt-ruby/tree/master - -[![Travis Build Status](https://travis-ci.org/codahale/bcrypt-ruby.svg?branch=master)](https://travis-ci.org/codahale/bcrypt-ruby) -[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/6fplerx9lnaf0hyo?svg=true)](https://ci.appveyor.com/project/TJSchuck35975/bcrypt-ruby) +* https://github.com/bcrypt-ruby/bcrypt-ruby/tree/master +[![Github Actions Build Status](https://github.com/bcrypt-ruby/bcrypt-ruby/actions/workflows/ruby.yml/badge.svg?branch=master)](https://github.com/bcrypt-ruby/bcrypt-ruby/actions/workflows/ruby.yml) ## Why you should use `bcrypt()` @@ -32,8 +30,8 @@ re-hash those passwords. This vulnerability only affected the JRuby gem. The bcrypt gem is available on the following Ruby platforms: * JRuby -* RubyInstaller 2.0 – 2.5 builds on Windows with the DevKit -* Any 2.0 – 2.5 Ruby on a BSD/OS X/Linux system with a compiler +* RubyInstaller builds on Windows with the DevKit +* Any modern Ruby on a BSD/OS X/Linux system with a compiler ## How to use `bcrypt()` in your Rails application diff --git a/Rakefile b/Rakefile index a47ce55..95e22a5 100644 --- a/Rakefile +++ b/Rakefile @@ -5,6 +5,8 @@ require 'rake/javaextensiontask' require 'rake/clean' require 'rdoc/task' require 'benchmark' +require "bundler" +Bundler::GemHelper.install_tasks CLEAN.include( "tmp", @@ -50,8 +52,8 @@ end if RUBY_PLATFORM =~ /java/ Rake::JavaExtensionTask.new('bcrypt_ext', GEMSPEC) do |ext| ext.ext_dir = 'ext/jruby' - ext.source_version = "1.7" - ext.target_version = "1.7" + ext.source_version = "1.8" + ext.target_version = "1.8" end else Rake::ExtensionTask.new("bcrypt_ext", GEMSPEC) do |ext| diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index e832e22..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,50 +0,0 @@ -version: "{branch}-{build}" -build: off -clone_depth: 1 - -init: - # Install Ruby head - - if %RUBY_VERSION%==head ( - appveyor DownloadFile https://github.com/oneclick/rubyinstaller2/releases/download/rubyinstaller-head/rubyinstaller-head-x86.exe -FileName C:\head_x86.exe & - C:\head_x86.exe /verysilent /dir=C:\Ruby%RUBY_VERSION% - ) - - if %RUBY_VERSION%==head-x64 ( - appveyor DownloadFile https://github.com/oneclick/rubyinstaller2/releases/download/rubyinstaller-head/rubyinstaller-head-x64.exe -FileName C:\head_x64.exe & - C:\head_x64.exe /verysilent /dir=C:\Ruby%RUBY_VERSION% - ) - - # Add Ruby to the path - - set PATH=C:\Ruby%RUBY_VERSION%\bin;%PATH% - -environment: - matrix: - - RUBY_VERSION: "head" - - RUBY_VERSION: "head-x64" - - RUBY_VERSION: "25" - - RUBY_VERSION: "25-x64" - - RUBY_VERSION: "24" - - RUBY_VERSION: "24-x64" - - RUBY_VERSION: "23" - - RUBY_VERSION: "23-x64" - - RUBY_VERSION: "22" - - RUBY_VERSION: "22-x64" - - RUBY_VERSION: "21" - - RUBY_VERSION: "21-x64" - - RUBY_VERSION: "200" - - RUBY_VERSION: "200-x64" - -install: - - ps: "Set-Content -Value 'gem: --no-ri --no-rdoc ' -Path C:\\ProgramData\\gemrc" - - if %RUBY_VERSION%==head ( gem install bundler -v'< 2' ) - - if %RUBY_VERSION%==head-x64 ( gem install bundler -v'< 2' ) - - bundle install - -before_build: - - ruby -v - - gem -v - -build_script: - - bundle exec rake compile -rdevkit - -test_script: - - bundle exec rake spec diff --git a/bcrypt.gemspec b/bcrypt.gemspec index b32cd08..b848c01 100644 --- a/bcrypt.gemspec +++ b/bcrypt.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'bcrypt' - s.version = '3.1.16' + s.version = '3.1.22' s.summary = "OpenBSD's bcrypt() password hashing algorithm." s.description = <<-EOF @@ -9,11 +9,13 @@ Gem::Specification.new do |s| passwords. EOF - s.files = `git ls-files`.split("\n") + s.files = Dir['CHANGELOG', 'COPYING', 'README.md', 'lib/**/*.rb', 'ext/**/*.*'] s.require_path = 'lib' - s.add_development_dependency 'rake-compiler', '~> 0.9.2' + s.add_development_dependency 'rake-compiler', '~> 1.2.0' s.add_development_dependency 'rspec', '>= 3' + s.add_development_dependency 'rdoc', '>= 7.0.3' + s.add_development_dependency 'benchmark', '>= 0.5.0' s.rdoc_options += ['--title', 'bcrypt-ruby', '--line-numbers', '--inline-source', '--main', 'README.md'] s.extra_rdoc_files += ['README.md', 'COPYING', 'CHANGELOG', *Dir['lib/**/*.rb']] @@ -22,6 +24,8 @@ Gem::Specification.new do |s| s.authors = ["Coda Hale"] s.email = "coda.hale@gmail.com" - s.homepage = "https://github.com/codahale/bcrypt-ruby" + s.homepage = "https://github.com/bcrypt-ruby/bcrypt-ruby" s.license = "MIT" + + s.metadata["changelog_uri"] = s.homepage + "/blob/master/CHANGELOG" end diff --git a/ext/jruby/bcrypt_jruby/BCrypt.java b/ext/jruby/bcrypt_jruby/BCrypt.java index 86db91b..bf987d9 100644 --- a/ext/jruby/bcrypt_jruby/BCrypt.java +++ b/ext/jruby/bcrypt_jruby/BCrypt.java @@ -688,20 +688,21 @@ static long roundsForLogRounds(int log_rounds) { */ private byte[] crypt_raw(byte password[], byte salt[], int log_rounds, boolean sign_ext_bug, int safety) { - int rounds, i, j; + long rounds; + int i, j; int cdata[] = bf_crypt_ciphertext.clone(); int clen = cdata.length; byte ret[]; if (log_rounds < 4 || log_rounds > 31) throw new IllegalArgumentException ("Bad number of rounds"); - rounds = 1 << log_rounds; + rounds = roundsForLogRounds(log_rounds); if (salt.length != BCRYPT_SALT_LEN) throw new IllegalArgumentException ("Bad salt length"); init_key(); ekskey(salt, password, sign_ext_bug, safety); - for (i = 0; i < rounds; i++) { + for (long r = 0; r < rounds; r++) { key(password, sign_ext_bug, safety); key(salt, false, safety); } diff --git a/ext/mri/bcrypt_ext.c b/ext/mri/bcrypt_ext.c index c8db933..3e5ac1c 100644 --- a/ext/mri/bcrypt_ext.c +++ b/ext/mri/bcrypt_ext.c @@ -1,59 +1,120 @@ #include #include +#ifdef HAVE_RUBY_THREAD_H +#include +#endif + static VALUE mBCrypt; static VALUE cBCryptEngine; +struct bc_salt_args { + const char * prefix; + unsigned long count; + const char * input; + int size; +}; + +static void * bc_salt_nogvl(void * ptr) { + struct bc_salt_args * args = ptr; + + return crypt_gensalt_ra(args->prefix, args->count, args->input, args->size); +} + /* Given a logarithmic cost parameter, generates a salt for use with +bc_crypt+. */ static VALUE bc_salt(VALUE self, VALUE prefix, VALUE count, VALUE input) { char * salt; VALUE str_salt; - - salt = crypt_gensalt_ra( - StringValuePtr(prefix), - NUM2ULONG(count), - NIL_P(input) ? NULL : StringValuePtr(input), - NIL_P(input) ? 0 : RSTRING_LEN(input)); + struct bc_salt_args args; + + /* duplicate the parameters for thread safety. If another thread has a + * reference to the parameters and mutates them while we are working, + * that would be very bad. Duping the strings means that the reference + * isn't shared. */ + prefix = rb_str_new_frozen(prefix); + input = rb_str_new_frozen(input); + + args.prefix = StringValueCStr(prefix); + args.count = NUM2ULONG(count); + args.input = NIL_P(input) ? NULL : StringValuePtr(input); + args.size = NIL_P(input) ? 0 : RSTRING_LEN(input); + +#ifdef HAVE_RUBY_THREAD_H + salt = rb_thread_call_without_gvl(bc_salt_nogvl, &args, NULL, NULL); +#else + salt = bc_salt_nogvl((void *)&args); +#endif if(!salt) return Qnil; str_salt = rb_str_new2(salt); + + RB_GC_GUARD(prefix); + RB_GC_GUARD(input); free(salt); return str_salt; } +struct bc_crypt_args { + const char * key; + const char * setting; + void * data; + int size; +}; + +static void * bc_crypt_nogvl(void * ptr) { + struct bc_crypt_args * args = ptr; + + return crypt_ra(args->key, args->setting, &args->data, &args->size); +} + /* Given a secret and a salt, generates a salted hash (which you can then store safely). */ static VALUE bc_crypt(VALUE self, VALUE key, VALUE setting) { char * value; - void * data; - int size; VALUE out; - data = NULL; - size = 0xDEADBEEF; + struct bc_crypt_args args; if(NIL_P(key) || NIL_P(setting)) return Qnil; - value = crypt_ra( - NIL_P(key) ? NULL : StringValuePtr(key), - NIL_P(setting) ? NULL : StringValuePtr(setting), - &data, - &size); + /* duplicate the parameters for thread safety. If another thread has a + * reference to the parameters and mutates them while we are working, + * that would be very bad. Duping the strings means that the reference + * isn't shared. */ + key = rb_str_new_frozen(key); + setting = rb_str_new_frozen(setting); + + args.data = NULL; + args.size = 0xDEADBEEF; + args.key = NIL_P(key) ? NULL : StringValueCStr(key); + args.setting = NIL_P(setting) ? NULL : StringValueCStr(setting); + +#ifdef HAVE_RUBY_THREAD_H + value = rb_thread_call_without_gvl(bc_crypt_nogvl, &args, NULL, NULL); +#else + value = bc_crypt_nogvl((void *)&args); +#endif - if(!value || !data) return Qnil; + if(!value || !args.data) return Qnil; out = rb_str_new2(value); - xfree(data); + RB_GC_GUARD(key); + RB_GC_GUARD(setting); + free(args.data); return out; } /* Create the BCrypt and BCrypt::Engine modules, and populate them with methods. */ void Init_bcrypt_ext(){ +#ifdef HAVE_RB_EXT_RACTOR_SAFE + rb_ext_ractor_safe(true); +#endif + mBCrypt = rb_define_module("BCrypt"); cBCryptEngine = rb_define_class_under(mBCrypt, "Engine", rb_cObject); diff --git a/lib/bcrypt/engine.rb b/lib/bcrypt/engine.rb index 2204843..1a253b3 100644 --- a/lib/bcrypt/engine.rb +++ b/lib/bcrypt/engine.rb @@ -7,6 +7,14 @@ class Engine MIN_COST = 4 # The maximum cost supported by the algorithm. MAX_COST = 31 + # Maximum possible size of bcrypt() secrets. + # Older versions of the bcrypt library would truncate passwords longer + # than 72 bytes, but newer ones do not. We truncate like the old library for + # forward compatibility. This way users upgrading from Ubuntu 18.04 to 20.04 + # will not have their user passwords invalidated, for example. + # A max secret length greater than 255 leads to bcrypt returning nil. + # https://github.com/bcrypt-ruby/bcrypt-ruby/issues/225#issuecomment-875908425 + MAX_SECRET_BYTESIZE = 72 # Maximum possible size of bcrypt() salts. MAX_SALT_LENGTH = 16 @@ -43,14 +51,23 @@ def self.cost=(cost) end # Given a secret and a valid salt (see BCrypt::Engine.generate_salt) calculates - # a bcrypt() password hash. + # a bcrypt() password hash. Secrets longer than 72 bytes are truncated. def self.hash_secret(secret, salt, _ = nil) + unless _.nil? + warn "[DEPRECATION] Passing the third argument to " \ + "`BCrypt::Engine.hash_secret` is deprecated. " \ + "Please do not pass the third argument which " \ + "is currently not used." + end + if valid_secret?(secret) if valid_salt?(salt) if RUBY_PLATFORM == "java" Java.bcrypt_jruby.BCrypt.hashpw(secret.to_s.to_java_bytes, salt.to_s) else - __bc_crypt(secret.to_s, salt) + secret = secret.to_s + secret = secret.byteslice(0, MAX_SECRET_BYTESIZE) if secret && secret.bytesize > MAX_SECRET_BYTESIZE + __bc_crypt(secret, salt) end else raise Errors::InvalidSalt.new("invalid salt") @@ -70,8 +87,7 @@ def self.generate_salt(cost = self.cost) if RUBY_PLATFORM == "java" Java.bcrypt_jruby.BCrypt.gensalt(cost) else - prefix = "$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW" - __bc_salt(prefix, cost, OpenSSL::Random.random_bytes(MAX_SALT_LENGTH)) + __bc_salt("$2a$", cost, OpenSSL::Random.random_bytes(MAX_SALT_LENGTH)) end else raise Errors::InvalidCost.new("cost must be numeric and > 0") @@ -80,7 +96,7 @@ def self.generate_salt(cost = self.cost) # Returns true if +salt+ is a valid bcrypt() salt, false if not. def self.valid_salt?(salt) - !!(salt =~ /^\$[0-9a-z]{2,}\$[0-9]{2,}\$[A-Za-z0-9\.\/]{22,}$/) + !!(salt =~ /\A\$[0-9a-z]{2,}\$[0-9]{2,}\$[A-Za-z0-9\.\/]{22,}\z/) end # Returns true if +secret+ is a valid bcrypt() secret, false if not. diff --git a/lib/bcrypt/password.rb b/lib/bcrypt/password.rb index 554967c..0c432d6 100644 --- a/lib/bcrypt/password.rb +++ b/lib/bcrypt/password.rb @@ -47,7 +47,7 @@ def create(secret, options = {}) end def valid_hash?(h) - /^\$[0-9a-z]{2}\$[0-9]{2}\$[A-Za-z0-9\.\/]{53}$/ === h + /\A\$[0-9a-z]{2}\$[0-9]{2}\$[A-Za-z0-9\.\/]{53}\z/ === h end end @@ -62,8 +62,28 @@ def initialize(raw_hash) end # Compares a potential secret against the hash. Returns true if the secret is the original secret, false otherwise. + # + # Comparison edge case/gotcha: + # + # secret = "my secret" + # @password = BCrypt::Password.create(secret) + # + # @password == secret # => True + # @password == @password # => False + # @password == @password.to_s # => False + # @password.to_s == @password # => True + # @password.to_s == @password.to_s # => True + # + # secret == @password # => probably False, because the secret is not a BCrypt::Password instance. def ==(secret) - super(BCrypt::Engine.hash_secret(secret, @salt)) + hash = BCrypt::Engine.hash_secret(secret, @salt) + + return false if hash.strip.empty? || strip.empty? || hash.bytesize != bytesize + + # Constant time comparison so they can't tell the length. + res = 0 + bytesize.times { |i| res |= getbyte(i) ^ hash.getbyte(i) } + res == 0 end alias_method :is_password?, :== @@ -83,5 +103,4 @@ def split_hash(h) return v.to_str, c.to_i, h[0, 29].to_str, mash[-31, 31].to_str end end - end diff --git a/spec/bcrypt/engine_spec.rb b/spec/bcrypt/engine_spec.rb index 90a681e..8abbe9f 100644 --- a/spec/bcrypt/engine_spec.rb +++ b/spec/bcrypt/engine_spec.rb @@ -1,4 +1,5 @@ require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper")) +require 'securerandom' describe 'BCrypt::Engine' do describe '.calibrate(upper_time_limit_in_ms)' do @@ -12,8 +13,11 @@ describe "The BCrypt engine" do specify "should calculate the optimal cost factor to fit in a specific time" do - first = BCrypt::Engine.calibrate(100) - second = BCrypt::Engine.calibrate(400) + start_time = Time.now + BCrypt::Password.create("testing testing", :cost => BCrypt::Engine::MIN_COST + 1) + min_time_ms = (Time.now - start_time) * 1000 + first = BCrypt::Engine.calibrate(min_time_ms) + second = BCrypt::Engine.calibrate(min_time_ms * 5) expect(second).to be > first end end @@ -154,4 +158,19 @@ class MyInvalidSecret expect(BCrypt::Engine.hash_secret(secret, salt)).to eql(test_vector) end end + + specify "should truncate long 1-byte character secrets to 72 bytes" do + # 'b' as a base triggers the failure at 256 characters, but 'a' does not. + too_long_secret = 'b'*(BCrypt::Engine::MAX_SECRET_BYTESIZE + 1) + just_right_secret = 'b'*BCrypt::Engine::MAX_SECRET_BYTESIZE + expect(BCrypt::Engine.hash_secret(too_long_secret, @salt)).to eq(BCrypt::Engine.hash_secret(just_right_secret, @salt)) + end + + specify "should truncate long multi-byte character secrets to 72 bytes" do + # 256 times causes bcrypt to return nil for libxcrypt > 4.4.18-4. + too_long_secret = '𐐷'*256 + # 𐐷 takes 4 bytes in UTF-8. 18 times is 72 bytes + just_right_secret = '𐐷'*18 + expect(BCrypt::Engine.hash_secret(too_long_secret, @salt)).to eq(BCrypt::Engine.hash_secret(just_right_secret, @salt)) + end end diff --git a/spec/bcrypt/password_spec.rb b/spec/bcrypt/password_spec.rb index f880f1c..5a93cbd 100644 --- a/spec/bcrypt/password_spec.rb +++ b/spec/bcrypt/password_spec.rb @@ -1,4 +1,5 @@ require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper")) +require 'securerandom' describe "Creating a hashed password" do @@ -26,6 +27,16 @@ expect { BCrypt::Password.create( "" ) }.not_to raise_error expect { BCrypt::Password.create( String.new ) }.not_to raise_error end + + specify "should tolerate very long string secrets" do + expect { BCrypt::Password.create("abcd"*1024) }.not_to raise_error + end + + specify "blows up when null bytes are in the string" do + # JRuby can handle the null bytes + skip if RUBY_ENGINE == 'jruby' + expect { BCrypt::Password.create( "foo\0bar".chop ) }.to raise_error + end end describe "Reading a hashed password" do @@ -108,6 +119,7 @@ describe "Validating a generated salt" do specify "should not accept an invalid salt" do expect(BCrypt::Engine.valid_salt?("invalid")).to eq(false) + expect(BCrypt::Engine.valid_salt?("invalid\n#{BCrypt::Engine.generate_salt}\ninvalid")).to eq(false) end specify "should accept a valid salt" do expect(BCrypt::Engine.valid_salt?(BCrypt::Engine.generate_salt)).to eq(true) @@ -117,6 +129,7 @@ describe "Validating a password hash" do specify "should not accept an invalid password" do expect(BCrypt::Password.valid_hash?("i_am_so_not_valid")).to be(false) + expect(BCrypt::Password.valid_hash?("invalid\n#{BCrypt::Password.create "i_am_so_valid"}\ninvalid")).to be(false) end specify "should accept a valid password" do expect(BCrypt::Password.valid_hash?(BCrypt::Password.create "i_am_so_valid")).to be(true)