diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b18fd29 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'weekly' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1905ff8 --- /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@v6 + + - 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..f0f2e10 --- /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@v6 + + - 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 621ca7f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: ruby -before_install: - - "echo 'gem: --no-rdoc --no-ri' > ~/.gemrc" - - gem update --system 2.7.8 - - gem install bundler -v 1.17.3 -rvm: - - 2.0 - - 2.1 - - 2.2 - - 2.3 - - 2.4 - - 2.5 - - 2.6 - - ruby-head - - jruby-head - - rbx-3 -script: bundle exec rake diff --git a/CHANGELOG b/CHANGELOG index 8bead60..a2c9982 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,94 +1,127 @@ -1.0.0 Feb 27 2007 - - Initial release. +3.1.22 Mar 18 2026 + - [CVE-2026-33306] Fix integer overflow in Java extension -2.0.0 Mar 07 2007 - - Removed BCrypt::Password#exactly_equals -- use BCrypt::Password#eql? instead. - - Added BCrypt::Password#is_password?. - - Refactored out BCrypt::Internals into more useful BCrypt::Engine. - - Added validation of secrets -- nil is not healthy. +3.1.21 Dec 31 2025 + - Use constant time comparisons + - Mark as Ractor safe -2.0.1 Mar 09 2007 - - Fixed load path issues - - Fixed crashes when hashing weird values (e.g., false, etc.) +3.1.20 Nov 17 2023 + - Limit packaged files -- decrease gem filesize by ~28% [GH #272 by @pusewicz] -2.0.2 Jun 06 2007 - - Fixed example code in the README [Winson] - - Fixed Solaris compatibility [Jeremy LaTrasse, Twitter crew] +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] -2.0.3 May 07 2008 - - Made exception classes descend from StandardError, not Exception [Dan42] - - Changed BCrypt::Engine.hash to BCrypt::Engine.hash_secret to avoid Merb - sorting issues. [Lee Pope] +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] -2.0.4 Mar 09 2009 - - Added Ruby 1.9 compatibility. [Genki Takiuchi] - - Fixed segfaults on some different types of empty strings. [Mike Pomraning] +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 -2.0.5 Mar 11 2009 - - Fixed Ruby 1.8.5 compatibility. [Mike Pomraning] +3.1.16 Sep 3 2020 + - Fix compilation on FreeBSD. [GH #234] -2.1.0 Aug 12 2009 - - Improved code coverage, unit tests, and build chain. [Hongli Lai] - - Ruby 1.9 compatibility fixes. [Hongli Lai] - - JRuby support, using Damien Miller's jBCrypt. [Hongli Lai] - - Ruby 1.9 GIL releasing for high-cost hashes. [Hongli Lai] +3.1.15 July 21 2020 + - Remove GVL optimization. Apparently it breaks things [GH #230] -2.1.1 Aug 14 2009 - - JVM 1.4/1.5 compatibility [Hongli Lai] +3.1.14 July 21 2020 + - Start calibration from the minimum cost supported by the algorithm [GH #206 by @sergey-alekseev] -2.1.2 Sep 16 2009 - - Fixed support for Solaris, OpenSolaris. +3.1.13 May 31 2019 + - No longer include compiled binaries for Windows. See GH #173. + - Update C and Java implementations to latest versions [GH #182 by @fonica] + - Bump default cost to 12 [GH #181 by @bdewater] + - Remove explicit support for Rubies 1.8 and 1.9 + - Define SKIP_GNU token when building extension (Fixes FreeBSD >= 12) [GH #189 by @adam12] -3.0.0 Aug 24 2011 - - Bcrypt C implementation replaced with a public domain implementation. - - License changed to MIT +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 - Fixes hash errors in Fedora 28 and Ubuntu 20 [GH #164 by @besser82] -3.0.1 Sep 12 2011 - - create raises an exception if the cost is higher than 31. GH #27 +3.1.11 Mar 06 2016 + - Add support for Ruby 2.2 in compiled Windows binaries -3.1.0 May 07 2013 - - Add BCrypt::Password.valid_hash?(str) to check if a string is a valid bcrypt password hash - - BCrypt::Password cost should be set to DEFAULT_COST if nil - - Add BCrypt::Engine.cost attribute for getting/setting a default cost externally +3.1.10 Jan 28 2015 + - Fix issue with dumping a BCrypt::Password instance to YAML in Ruby 2.2 [GH #107 by @mattwildig] -3.1.1 Jul 10 2013 - - Remove support for Ruby 1.8 in compiled win32 binaries +3.1.9 Oct 23 2014 + - Rebuild corrupt binaries -3.1.2 Aug 26 2013 - - Add support for Ruby 1.8 and 2.0 (in addition to 1.9) in compiled Windows binaries - - Add support for 64-bit Windows +3.1.8 Oct 23 2014 + - Add support for Ruby 2.1 in compiled Windows binaries [GH #102] -3.1.3 Feb 21 2014 - - Add support for Ruby 2.1 in compiled Windows binaries - - Rename gem from "bcrypt-ruby" to just "bcrypt". [GH #86 by @sferik] +3.1.7 Feb 24 2014 + - Rebuild corrupt Java binary version of gem [GH #90] + - The 2.1 support for Windows binaries alleged in 3.1.3 was a lie -- documentation removed 3.1.6 Feb 21 2014 - Dummy version of "bcrypt-ruby" needed a couple version bumps to fix some bugs. It felt wrong to have that at a higher version than the real gem, so the real gem is getting bumped to 3.1.6. -3.1.7 Feb 24 2014 - - Rebuild corrupt Java binary version of gem [GH #90] - - The 2.1 support for Windows binaries alleged in 3.1.3 was a lie -- documentation removed +3.1.3 Feb 21 2014 + - Add support for Ruby 2.1 in compiled Windows binaries + - Rename gem from "bcrypt-ruby" to just "bcrypt". [GH #86 by @sferik] -3.1.8 Oct 23 2014 - - Add support for Ruby 2.1 in compiled Windows binaries [GH #102] +3.1.2 Aug 26 2013 + - Add support for Ruby 1.8 and 2.0 (in addition to 1.9) in compiled Windows binaries + - Add support for 64-bit Windows -3.1.9 Oct 23 2014 - - Rebuild corrupt binaries +3.1.1 Jul 10 2013 + - Remove support for Ruby 1.8 in compiled win32 binaries -3.1.10 Jan 28 2015 - - Fix issue with dumping a BCrypt::Password instance to YAML in Ruby 2.2 [GH #107 by @mattwildig] +3.1.0 May 07 2013 + - Add BCrypt::Password.valid_hash?(str) to check if a string is a valid bcrypt password hash + - BCrypt::Password cost should be set to DEFAULT_COST if nil + - Add BCrypt::Engine.cost attribute for getting/setting a default cost externally -3.1.11 Mar 06 2016 - - Add support for Ruby 2.2 in compiled Windows binaries +3.0.1 Sep 12 2011 + - create raises an exception if the cost is higher than 31. GH #27 -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] +3.0.0 Aug 24 2011 + - Bcrypt C implementation replaced with a public domain implementation. + - License changed to MIT -[DRAFT] 4.0.0 MMM DD YYYY - - No longer include compiled binaries for Windows. See GH #173. - - Update C and Java implementations to latest versions [GH #182 by @fonica] - - Bump default cost to 12 [GH #181 by @bdewater] - - Remove explicit support for Rubies 1.8 and 1.9 +2.1.2 Sep 16 2009 + - Fixed support for Solaris, OpenSolaris. + +2.1.1 Aug 14 2009 + - JVM 1.4/1.5 compatibility [Hongli Lai] + +2.1.0 Aug 12 2009 + - Improved code coverage, unit tests, and build chain. [Hongli Lai] + - Ruby 1.9 compatibility fixes. [Hongli Lai] + - JRuby support, using Damien Miller's jBCrypt. [Hongli Lai] + - Ruby 1.9 GIL releasing for high-cost hashes. [Hongli Lai] + +2.0.5 Mar 11 2009 + - Fixed Ruby 1.8.5 compatibility. [Mike Pomraning] + +2.0.4 Mar 09 2009 + - Added Ruby 1.9 compatibility. [Genki Takiuchi] + - Fixed segfaults on some different types of empty strings. [Mike Pomraning] + +2.0.3 May 07 2008 + - Made exception classes descend from StandardError, not Exception [Dan42] + - Changed BCrypt::Engine.hash to BCrypt::Engine.hash_secret to avoid Merb + sorting issues. [Lee Pope] + +2.0.2 Jun 06 2007 + - Fixed example code in the README [Winson] + - Fixed Solaris compatibility [Jeremy LaTrasse, Twitter crew] + +2.0.1 Mar 09 2007 + - Fixed load path issues + - Fixed crashes when hashing weird values (e.g., false, etc.) + +2.0.0 Mar 07 2007 + - Removed BCrypt::Password#exactly_equals -- use BCrypt::Password#eql? instead. + - Added BCrypt::Password#is_password?. + - Refactored out BCrypt::Internals into more useful BCrypt::Engine. + - Added validation of secrets -- nil is not healthy. + +1.0.0 Feb 27 2007 + - Initial release. diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 9981158..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,37 +0,0 @@ -PATH - remote: . - specs: - bcrypt (3.1.13) - -GEM - remote: https://rubygems.org/ - specs: - diff-lcs (1.3) - rake (12.3.2) - rake-compiler (0.9.9) - rake - rspec (3.8.0) - rspec-core (~> 3.8.0) - rspec-expectations (~> 3.8.0) - rspec-mocks (~> 3.8.0) - rspec-core (3.8.0) - rspec-support (~> 3.8.0) - rspec-expectations (3.8.3) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-mocks (3.8.0) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-support (3.8.0) - -PLATFORMS - java - ruby - -DEPENDENCIES - bcrypt! - rake-compiler (~> 0.9.2) - rspec (>= 3) - -BUNDLED WITH - 1.16.1 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 98ed63b..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,6 +52,8 @@ end if RUBY_PLATFORM =~ /java/ Rake::JavaExtensionTask.new('bcrypt_ext', GEMSPEC) do |ext| ext.ext_dir = 'ext/jruby' + 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 e67bad2..b848c01 100644 --- a/bcrypt.gemspec +++ b/bcrypt.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'bcrypt' - s.version = '3.1.13' + 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 b2edd1d..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); - xfree(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) 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/ext/mri/crypt_blowfish.c b/ext/mri/crypt_blowfish.c index 9d3f3be..5760064 100644 --- a/ext/mri/crypt_blowfish.c +++ b/ext/mri/crypt_blowfish.c @@ -361,7 +361,7 @@ static BF_ctx BF_init_state = { } }; -static unsigned char BF_itoa64[64 + 1] = +static const unsigned char BF_itoa64[64 + 1] = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; static unsigned char BF_atoi64[0x60] = { @@ -387,9 +387,8 @@ static int BF_decode(BF_word *dst, const char *src, int size) unsigned char *dptr = (unsigned char *)dst; unsigned char *end = dptr + size; const unsigned char *sptr = (const unsigned char *)src; - unsigned int tmp, c1, c2, c3, c4; - do { + unsigned int tmp, c1, c2, c3, c4; BF_safe_atoi64(c1, *sptr++); BF_safe_atoi64(c2, *sptr++); *dptr++ = (c1 << 2) | ((c2 & 0x30) >> 4); @@ -402,7 +401,6 @@ static int BF_decode(BF_word *dst, const char *src, int size) BF_safe_atoi64(c4, *sptr++); *dptr++ = ((c3 & 0x03) << 6) | c4; } while (dptr < end); - return 0; } @@ -411,9 +409,8 @@ static void BF_encode(char *dst, const BF_word *src, int size) const unsigned char *sptr = (const unsigned char *)src; const unsigned char *end = sptr + size; unsigned char *dptr = (unsigned char *)dst; - unsigned int c1, c2; - do { + unsigned int c1, c2; c1 = *sptr++; *dptr++ = BF_itoa64[c1 >> 2]; c1 = (c1 & 0x03) << 4; @@ -442,10 +439,9 @@ static void BF_swap(BF_word *x, int count) { static int endianness_check = 1; char *is_little_endian = (char *)&endianness_check; - BF_word tmp; - if (*is_little_endian) do { + BF_word tmp; tmp = *x; tmp = (tmp << 16) | (tmp >> 16); *x++ = ((tmp & 0x00FF00FF) << 8) | ((tmp >> 8) & 0x00FF00FF); @@ -517,7 +513,7 @@ static void BF_swap(BF_word *x, int count) R = L; \ L = tmp4 ^ data.ctx.P[BF_N + 1]; -#if BF_ASM +#if BF_ASM == 1 #define BF_body() \ _BF_body_r(&data.ctx); #else @@ -650,7 +646,7 @@ static char *BF_crypt(const char *key, const char *setting, char *output, int size, BF_word min) { -#if BF_ASM +#if BF_ASM == 1 extern void _BF_body_r(BF_ctx *ctx); #endif struct { diff --git a/ext/mri/crypt_gensalt.c b/ext/mri/crypt_gensalt.c index 73c15a1..744912a 100644 --- a/ext/mri/crypt_gensalt.c +++ b/ext/mri/crypt_gensalt.c @@ -28,7 +28,7 @@ /* Just to make sure the prototypes match the actual definitions */ #include "crypt_gensalt.h" -unsigned char _crypt_itoa64[64 + 1] = +const unsigned char _crypt_itoa64[64 + 1] = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; char *_crypt_gensalt_traditional_rn(const char *prefix, unsigned long count, diff --git a/ext/mri/crypt_gensalt.h b/ext/mri/crypt_gensalt.h index 457bbfe..3a5ca08 100644 --- a/ext/mri/crypt_gensalt.h +++ b/ext/mri/crypt_gensalt.h @@ -17,7 +17,7 @@ #ifndef _CRYPT_GENSALT_H #define _CRYPT_GENSALT_H -extern unsigned char _crypt_itoa64[]; +extern const unsigned char _crypt_itoa64[]; extern char *_crypt_gensalt_traditional_rn(const char *prefix, unsigned long count, const char *input, int size, char *output, int output_size); diff --git a/ext/mri/wrapper.c b/ext/mri/wrapper.c index 1e49c90..cbba882 100644 --- a/ext/mri/wrapper.c +++ b/ext/mri/wrapper.c @@ -17,6 +17,9 @@ #include #include +/* Redefine strdup to ruby_strdup in case string.h doesn't export it. */ +#include + #include #ifndef __set_errno #define __set_errno(val) errno = (val) @@ -176,7 +179,7 @@ char *crypt_ra(const char *key, const char *setting, return _crypt_blowfish_rn(key, setting, (char *)*data, *size); } -char *crypt_r(const char *key, const char *setting, void *data) +char *crypt_r(const char *key, const char *setting, struct crypt_data *data) { return _crypt_retval_magic( crypt_rn(key, setting, data, CRYPT_OUTPUT_SIZE), diff --git a/ext/mri/x86.S b/ext/mri/x86.S index b0f1cd2..4c5c069 100644 --- a/ext/mri/x86.S +++ b/ext/mri/x86.S @@ -199,5 +199,5 @@ BF_die: #endif #if defined(__ELF__) && defined(__linux__) -.section .note.GNU-stack,"",@progbits +.section .note.GNU-stack,"",%progbits #endif diff --git a/lib/bcrypt/engine.rb b/lib/bcrypt/engine.rb index 226d142..1a253b3 100644 --- a/lib/bcrypt/engine.rb +++ b/lib/bcrypt/engine.rb @@ -5,6 +5,16 @@ class Engine DEFAULT_COST = 12 # The minimum cost supported by the algorithm. 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 @@ -41,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") @@ -68,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") @@ -78,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. @@ -99,7 +117,7 @@ def self.valid_secret?(secret) # # should take less than 1000ms # BCrypt::Password.create("woo", :cost => 12) def self.calibrate(upper_time_limit_in_ms) - 40.times do |i| + (BCrypt::Engine::MIN_COST..BCrypt::Engine::MAX_COST-1).each do |i| start_time = Time.now Password.create("testing testing", :cost => i+1) end_time = Time.now - start_time diff --git a/lib/bcrypt/password.rb b/lib/bcrypt/password.rb index 509a8d9..0c432d6 100644 --- a/lib/bcrypt/password.rb +++ b/lib/bcrypt/password.rb @@ -42,12 +42,12 @@ class << self # @password = BCrypt::Password.create("my secret", :cost => 13) def create(secret, options = {}) cost = options[:cost] || BCrypt::Engine.cost - raise ArgumentError if cost > 31 + raise ArgumentError if cost > BCrypt::Engine::MAX_COST Password.new(BCrypt::Engine.hash_secret(secret, BCrypt::Engine.generate_salt(cost))) end def valid_hash?(h) - h =~ /^\$[0-9a-z]{2}\$[0-9]{2}\$[A-Za-z0-9\.\/]{53}$/ + /\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 cde842c..8abbe9f 100644 --- a/spec/bcrypt/engine_spec.rb +++ b/spec/bcrypt/engine_spec.rb @@ -1,9 +1,23 @@ 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 + context 'a tiny upper time limit provided' do + it 'returns a minimum cost supported by the algorithm' do + expect(BCrypt::Engine.calibrate(0.001)).to eq(4) + end + end + end +end 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 @@ -144,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 648e614..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) @@ -116,9 +128,10 @@ 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_falsey + 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_truthy + expect(BCrypt::Password.valid_hash?(BCrypt::Password.create "i_am_so_valid")).to be(true) end end