From acbbf925b8f0fe11c2b4873c5c7d3db496f43987 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Fri, 26 Mar 2021 19:42:39 -0700 Subject: [PATCH 01/35] Clean up README line length --- README.md | 126 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 96 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 089b71f..40ed25d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # Ruby Argon2 Gem -This Ruby Gem provides FFI bindings, and a simplified interface, to the Argon2 algorithm. [Argon2](https://github.com/P-H-C/phc-winner-argon2) is the official winner of the Password Hashing Competition, a several year project to identify a successor to bcrypt/PBKDF/scrypt methods of securely storing passwords. This is an independant project and not official from the PHC team. +This Ruby Gem provides FFI bindings, and a simplified interface, to the Argon2 +algorithm. [Argon2](https://github.com/P-H-C/phc-winner-argon2) is the official +winner of the Password Hashing Competition, a several year project to identify a +successor to bcrypt/PBKDF/scrypt methods of securely storing passwords. This is +an independant project and not official from the PHC team. ![Build Status](https://github.com/technion/ruby-argon2/workflows/Test%20Suite/badge.svg) [![Code Climate](https://codeclimate.com/github/technion/ruby-argon2/badges/gpa.svg)](https://codeclimate.com/github/technion/ruby-argon2) @@ -10,15 +14,29 @@ This Ruby Gem provides FFI bindings, and a simplified interface, to the Argon2 a This project has several key tenets to its design: -* The reference Argon2 implementation is to be used "unaltered". To ensure compliance with this goal, and encourage regular updates from upstream, the upstream library is implemented as a git submodule, and is intended to stay that way. -* The FFI interface is kept as slim as possible, with wrapper classes preferred to implementing context structs in FFI -* Security and maintainability take top priority. This can have an impact on platform support. A PR that contains platform specific code paths is unlikely to be accepted. -* Tested platforms are MRI Ruby 2.2, 2.3 and JRuby 9000. No assertions are made on other platforms. -* Errors from the C interface are raised as Exceptions. There are a lot of exception classes, but they tend to relate to things like very broken input, and code bugs. Calls to this library should generally not require a rescue. +* The reference Argon2 implementation is to be used "unaltered". To ensure + compliance with this goal, and encourage regular updates from upstream, the + upstream library is implemented as a git submodule, and is intended to stay + that way. +* The FFI interface is kept as slim as possible, with wrapper classes preferred + to implementing context structs in FFI +* Security and maintainability take top priority. This can have an impact on + platform support. A PR that contains platform specific code paths is unlikely + to be accepted. +* Tested platforms are MRI Ruby 2.2, 2.3 and JRuby 9000. No assertions are made + on other platforms. +* Errors from the C interface are raised as Exceptions. There are a lot of + exception classes, but they tend to relate to things like very broken input, + and code bugs. Calls to this library should generally not require a rescue. * Test suites should aim for 100% code coverage. -* Default work values should not be considered constants. I will increase them from time to time. -* Not exposing the threads parameter is a design choice. I believe there is significant risk, and minimal gain in using a value other than '1'. Four threads on a four core box completely ties up the entire server to process one user logon. If you want more security, increase m_cost. -* Many Rubocop errors have been disabled, but any commit should avoid new alerts or demonstrate their necessity. +* Default work values should not be considered constants. I will increase them + from time to time. +* Not exposing the threads parameter is a design choice. I believe there is + significant risk, and minimal gain in using a value other than '1'. Four + threads on a four core box completely ties up the entire server to process one + user logon. If you want more security, increase m_cost. +* Many Rubocop errors have been disabled, but any commit should avoid new alerts + or demonstrate their necessity. ## Usage @@ -43,7 +61,11 @@ hasher = Argon2::Password.new hasher.create("password") ``` -If you follow this pattern, it is important to create a new `Argon2::Password` every time you generate a hash, in order to ensure a unique salt. See [issue 23](https://github.com/technion/ruby-argon2/issues/23) for more information. +If you follow this pattern, it is important to create a new `Argon2::Password` +every time you generate a hash, in order to ensure a unique salt. See +[issue 23](https://github.com/technion/ruby-argon2/issues/23) for more +information. + Alternatively, use this shortcut: ```ruby @@ -51,7 +73,8 @@ Argon2::Password.create("password") => "$argon2i$v=19$m=65536,t=2,p=1$61qkSyYNbUgf3kZH3GtHRw$4CQff9AZ0lWd7uF24RKMzqEiGpzhte1Hp8SO7X8bAew" ``` -You can then use this function to verify a password against a given hash. Will return either true or false. +You can then use this function to verify a password against a given hash. Will +return either true or false. ```ruby Argon2::Password.verify_password("password", secure_password) @@ -64,7 +87,9 @@ Argon2::Password.verify_password("password", "$argon2id$v=19$m=262144,t=2,p=1$c2 => true ``` -Argon2 supports an optional key value. This should be stored securely on your server, such as alongside your database credentials. Hashes generated with a key will only validate when presented that key. +Argon2 supports an optional key value. This should be stored securely on your +server, such as alongside your database credentials. Hashes generated with a key +will only validate when presented that key. ```ruby KEY = "A key" @@ -72,58 +97,98 @@ argon = Argon2::Password.new(t_cost: 2, m_cost: 16, secret: KEY) myhash = argon.create("A password") Argon2::Password.verify_password("A password", myhash, KEY) ``` + ## Version 2.0 - Argon 2id -Version 2.x upwards will now default to the Argon2id hash format. This is consistent with current recommendations regarding Argon2 usage. It remains capable of verifying existing hashes. + +Version 2.x upwards will now default to the Argon2id hash format. This is +consistent with current recommendations regarding Argon2 usage. It remains +capable of verifying existing hashes. ## Important notes regarding version 1.0 upgrade -Version 1.0.0 included a major version bump over 0.1.4 due to several breaking changes. The first of these was an API change, which you can read the background on [here](https://github.com/technion/ruby-argon2/issues/9). -The second of these is that the reference Argon2 implementation introduced an algorithm change, which produces a hash which is not backwards compatible. This is documented on [this PR on the C library](https://github.com/P-H-C/phc-winner-argon2/pull/115). This was a regrettable requirement to address a security concern in the algorithm itself. The two versions of the Argon2 algorithm are numbered 1.0 and 1.3 respectively. +Version 1.0.0 included a major version bump over 0.1.4 due to several breaking +changes. The first of these was an API change, which you can read the background +on [here](https://github.com/technion/ruby-argon2/issues/9). -Shortly after this, version 1.0.0 of this gem was released with this breaking change, supporting only Argon2 v1.3. Further time later, the official encoding format was updated, with a spec that included the version number, and the library introduced backward compatibility. This should remove the likelihood of such breaking changes in future. Version 1.1.0 will silently introduce the current version number in hashes, in order to avoid a further compatibility break. +The second of these is that the reference Argon2 implementation introduced an +algorithm change, which produces a hash which is not backwards compatible. This +is documented on +[this PR on the C library](https://github.com/P-H-C/phc-winner-argon2/pull/115). +This was a regrettable requirement to address a security concern in the +algorithm itself. The two versions of the Argon2 algorithm are numbered 1.0 and +1.3 respectively. + +Shortly after this, version 1.0.0 of this gem was released with this breaking +change, supporting only Argon2 v1.3. Further time later, the official encoding +format was updated, with a spec that included the version number, and the +library introduced backward compatibility. This should remove the likelihood of +such breaking changes in future. Version 1.1.0 will silently introduce the +current version number in hashes, in order to avoid a further compatibility +break. ## Platform Issues -The default installation workflow has caused issues with a number of gems under the latest OSX. There is some excellent documentation on the issue and some workarounds in the [Jekyll Documentation](http://jekyllrb.com/docs/troubleshooting/#jekyll-amp-mac-os-x-1011). With this in mind, OSX is a fully supported OS. +The default installation workflow has caused issues with a number of gems under +the latest OSX. There is some excellent documentation on the issue and some +workarounds in the +[Jekyll Documentation](http://jekyllrb.com/docs/troubleshooting/#jekyll-amp-mac-os-x-1011). +With this in mind, OSX is a fully supported OS. -Windows is not. Nobody anywhere has the resources to support Ruby FFI code on Windows. +Windows is not. Nobody anywhere has the resources to support Ruby FFI code on +Windows. -grsec introduces certain challenges. Please see [documentation here](https://github.com/technion/ruby-argon2/issues/15). +grsec introduces certain challenges. Please see +[documentation here](https://github.com/technion/ruby-argon2/issues/15). See the .travis.yml file to see currently tested and supported Ruby versions. ## RubyDocs documentation -[The usual URL](http://www.rubydoc.info/gems/argon2) will provide detailed documentation. +[The usual URL](http://www.rubydoc.info/gems/argon2) will provide detailed +documentation. ## FAQ ### Don't roll your own crypto! -This gets its own section because someone will raise it. I did not invent or alter this algorithm, or implement it directly. These bindings were written following [considerable involvement with the C reference](https://github.com/P-H-C/phc-winner-argon2/commits/master?author=technion), and a strong focus has been made on following the intent of the algorithm. +This gets its own section because someone will raise it. I did not invent or +alter this algorithm, or implement it directly. These bindings were written +following +[considerable involvement with the C reference](https://github.com/P-H-C/phc-winner-argon2/commits/master?author=technion), +and a strong focus has been made on following the intent of the algorithm. -It is strongly advised to avoid implementations that utilise off-spec methods of introducing salts, invent imaginary parameters, or which use the word "encryption" in describing the password hashing process +It is strongly advised to avoid implementations that utilise off-spec methods of +introducing salts, invent imaginary parameters, or which use the word +"encryption" in describing the password hashing process ### Secure wipe is useless -Although the low level C contains support for "secure memory wipe", any code hitting that layer has copied your password to a dozen places in memory. It should be assumed that such functionality does not exist. +Although the low level C contains support for "secure memory wipe", any code +hitting that layer has copied your password to a dozen places in memory. It +should be assumed that such functionality does not exist. ### Work maximums may be tighter than reference -The reference implementation is aimed to provide secure hashing for many years. This implementation doesn't want you to DoS yourself in the meantime. Accordingly, some artificial limits exist on work powers. This gem can be much more agile in raising these as technology progresses. +The reference implementation is aimed to provide secure hashing for many years. +This implementation doesn't want you to DoS yourself in the meantime. +Accordingly, some artificial limits exist on work powers. This gem can be much +more agile in raising these as technology progresses. ### Salts in general -If you are providing your own salt, you are probably using it wrong. The design of any secure hashing system should take care of it for you. +If you are providing your own salt, you are probably using it wrong. The design +of any secure hashing system should take care of it for you. ## Contributing -Any form of contribution is appreciated, however, please review [CONTRIBUTING.md](CONTRIBUTING.md). +Any form of contribution is appreciated, however, please review +[CONTRIBUTING.md](CONTRIBUTING.md). ## Building locally/Tests -To build the gem locally, you will need to checkout the submodule and build it manually: +To build the gem locally, you will need to checkout the submodule and build it +manually: ```shell git submodule update --init --recursive @@ -133,7 +198,8 @@ make cd ../.. ``` -The test harness includes a property based test. To more strenuously perform this test, you can tune the iterations parameter: +The test harness includes a property based test. To more strenuously perform +this test, you can tune the iterations parameter: ```shell TEST_CHECKS=10000 bundle exec rake test @@ -141,5 +207,5 @@ TEST_CHECKS=10000 bundle exec rake test ## License -The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). - +The gem is available as open source under the terms of the +[MIT License](http://opensource.org/licenses/MIT). From 37bcd26754671ff92d42671fdc4cc1a8b1db5804 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Fri, 26 Mar 2021 20:03:22 -0700 Subject: [PATCH 02/35] Remove autogenerated comment from Gemfile --- Gemfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Gemfile b/Gemfile index ea62c3a..5f10ba8 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,4 @@ # frozen_string_literal: true source 'https://rubygems.org' - -# Specify your gem's dependencies in argon2.gemspec gemspec From fd6b4f3ea28f31ce90d7f140986b76aabd7ba07d Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Fri, 26 Mar 2021 20:20:07 -0700 Subject: [PATCH 03/35] Clean up yard documentation and move password class to its own file --- README.md | 2 +- lib/argon2.rb | 51 ++++++++-------------------------------- lib/argon2/constants.rb | 3 ++- lib/argon2/engine.rb | 7 +++++- lib/argon2/errors.rb | 14 +++++++---- lib/argon2/ffi_engine.rb | 7 +++++- lib/argon2/password.rb | 42 +++++++++++++++++++++++++++++++++ lib/argon2/version.rb | 5 ++-- 8 files changed, 80 insertions(+), 51 deletions(-) create mode 100644 lib/argon2/password.rb diff --git a/README.md b/README.md index 40ed25d..4b4844b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This Ruby Gem provides FFI bindings, and a simplified interface, to the Argon2 algorithm. [Argon2](https://github.com/P-H-C/phc-winner-argon2) is the official winner of the Password Hashing Competition, a several year project to identify a successor to bcrypt/PBKDF/scrypt methods of securely storing passwords. This is -an independant project and not official from the PHC team. +an independent project and not official from the PHC team. ![Build Status](https://github.com/technion/ruby-argon2/workflows/Test%20Suite/badge.svg) [![Code Climate](https://codeclimate.com/github/technion/ruby-argon2/badges/gpa.svg)](https://codeclimate.com/github/technion/ruby-argon2) diff --git a/lib/argon2.rb b/lib/argon2.rb index 2d3ba7c..e4234da 100644 --- a/lib/argon2.rb +++ b/lib/argon2.rb @@ -1,48 +1,17 @@ # frozen_string_literal: true +## +# This Ruby Gem provides FFI bindings, and a simplified interface, to the Argon2 +# algorithm. Argon2 is the official winner of the Password Hashing Competition, +# a several year project to identify a successor to bcrypt/PBKDF/scrypt methods +# of securely storing passwords. This is an independent project and not official +# from the PHC team. +# +module Argon2; end + require 'argon2/constants' require 'argon2/ffi_engine' require 'argon2/version' require 'argon2/errors' require 'argon2/engine' - -module Argon2 - # Front-end API for the Argon2 module. - class Password - def initialize(options = {}) - @t_cost = options[:t_cost] || 2 - raise ArgonHashFail, "Invalid t_cost" if @t_cost < 1 || @t_cost > 750 - - @m_cost = options[:m_cost] || 16 - raise ArgonHashFail, "Invalid m_cost" if @m_cost < 1 || @m_cost > 31 - - @salt = options[:salt_do_not_supply] || Engine.saltgen - @secret = options[:secret] - end - - def create(pass) - raise ArgonHashFail, "Invalid password (expected string)" unless - pass.is_a?(String) - - Argon2::Engine.hash_argon2id_encode( - pass, @salt, @t_cost, @m_cost, @secret) - end - - # Helper class, just creates defaults and calls hash() - def self.create(pass) - argon2 = Argon2::Password.new - argon2.create(pass) - end - - # Supports 1 and argon2id formats. - def self.valid_hash?(hash) - /^\$argon2(id?|d).{,113}/ =~ hash - end - - def self.verify_password(pass, hash, secret = nil) - raise ArgonHashFail, "Invalid hash" unless valid_hash?(hash) - - Argon2::Engine.argon2_verify(pass, hash, secret) - end - end -end +require 'argon2/password' diff --git a/lib/argon2/constants.rb b/lib/argon2/constants.rb index daafa93..ac62a02 100644 --- a/lib/argon2/constants.rb +++ b/lib/argon2/constants.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true module Argon2 + ## # Constants utilised in several parts of the Argon2 module # SALT_LEN is a standard recommendation from the Argon2 spec. - + # module Constants SALT_LEN = 16 OUT_LEN = 32 # Binary, unencoded output diff --git a/lib/argon2/engine.rb b/lib/argon2/engine.rb index 06e23fc..bacea50 100644 --- a/lib/argon2/engine.rb +++ b/lib/argon2/engine.rb @@ -3,8 +3,13 @@ require 'securerandom' module Argon2 - # Generates a random, binary string for use as a salt. + ## + # TODO: Document Engine class, and how it differs from the ffi_engine class. + # class Engine + ## + # Generates a random, binary string for use as a salt. + # def self.saltgen SecureRandom.random_bytes(Argon2::Constants::SALT_LEN) end diff --git a/lib/argon2/errors.rb b/lib/argon2/errors.rb index b64abdb..4f90b8a 100644 --- a/lib/argon2/errors.rb +++ b/lib/argon2/errors.rb @@ -1,10 +1,16 @@ # frozen_string_literal: true -# Defines an array of errors that matches the enum list of errors from -# argon2.h. This allows return values to propagate errors through the FFI. - module Argon2 + ## + # Generic error for applications to catch when using Argon2::Engine and + # Argon2::Password + # class ArgonHashFail < StandardError; end + + ## + # Defines an array of errors that matches the enum list of errors from + # argon2.h. This allows return values to propagate errors through the FFI. + # ERRORS = %w[ ARGON2_OK ARGON2_OUTPUT_PTR_NULL @@ -40,5 +46,5 @@ class ArgonHashFail < StandardError; end ARGON2_ENCODING_FAIL ARGON2_DECODING_FAIL ARGON2_THREAD_FAIL - ].freeze + ].freeze end diff --git a/lib/argon2/ffi_engine.rb b/lib/argon2/ffi_engine.rb index 7258042..be21898 100644 --- a/lib/argon2/ffi_engine.rb +++ b/lib/argon2/ffi_engine.rb @@ -4,7 +4,10 @@ require 'ffi-compiler/loader' module Argon2 - # Direct external bindings. Call these methods via the Engine class to ensure points are dealt with + ## + # Direct external bindings. Call these methods via the Engine class to ensure + # points are dealt with. + # module Ext extend FFI::Library ffi_lib FFI::Compiler::Loader.find(FFI::Platform.windows? ? 'libargon2_wrap' : 'argon2_wrap') @@ -43,8 +46,10 @@ module Ext pointer size_t], :int, :blocking => true end + ## # The engine class shields users from the FFI interface. # It is generally not advised to directly use this class. + # class Engine def self.hash_argon2i(password, salt, t_cost, m_cost, out_len = nil) out_len = (out_len || Constants::OUT_LEN).to_i diff --git a/lib/argon2/password.rb b/lib/argon2/password.rb new file mode 100644 index 0000000..4d92672 --- /dev/null +++ b/lib/argon2/password.rb @@ -0,0 +1,42 @@ +module Argon2 + ## + # Front-end API for the Argon2 module. + # + class Password + def initialize(options = {}) + @t_cost = options[:t_cost] || 2 + raise ArgonHashFail, "Invalid t_cost" if @t_cost < 1 || @t_cost > 750 + + @m_cost = options[:m_cost] || 16 + raise ArgonHashFail, "Invalid m_cost" if @m_cost < 1 || @m_cost > 31 + + @salt = options[:salt_do_not_supply] || Engine.saltgen + @secret = options[:secret] + end + + def create(pass) + raise ArgonHashFail, "Invalid password (expected string)" unless + pass.is_a?(String) + + Argon2::Engine.hash_argon2id_encode( + pass, @salt, @t_cost, @m_cost, @secret) + end + + # Helper class, just creates defaults and calls hash() + def self.create(pass) + argon2 = Argon2::Password.new + argon2.create(pass) + end + + # Supports 1 and argon2id formats. + def self.valid_hash?(hash) + /^\$argon2(id?|d).{,113}/ =~ hash + end + + def self.verify_password(pass, hash, secret = nil) + raise ArgonHashFail, "Invalid hash" unless valid_hash?(hash) + + Argon2::Engine.argon2_verify(pass, hash, secret) + end + end +end diff --git a/lib/argon2/version.rb b/lib/argon2/version.rb index 3ff3d13..8676113 100644 --- a/lib/argon2/version.rb +++ b/lib/argon2/version.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true -# Standard Gem version constant. - module Argon2 + ## + # Standard Gem version constant. + # VERSION = "2.0.3" end From 3d3531b00aa91b0c3124d0385dab5c221d131b76 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sat, 27 Mar 2021 12:45:34 -0700 Subject: [PATCH 04/35] Refactor errors to allow better exception handling This will require end users to update their error handling if they have any, but it's an easy one-line find/replace for existing applications, and the helpers will be breaking changes as well anyway, so might as well take full advantage of the major version bump. --- .github/workflows/rubocop.yml | 1 - .github/workflows/ruby.yml | 2 +- Changelog.md | 6 ++++++ README.md | 5 +++++ argon2.gemspec | 7 +++++++ lib/argon2/errors.rb | 36 ++++++++++++++++++++++++++++++++--- lib/argon2/ffi_engine.rb | 14 +++++++------- lib/argon2/password.rb | 22 ++++++++++----------- lib/argon2/version.rb | 2 +- test/error_test.rb | 8 ++++---- 10 files changed, 75 insertions(+), 28 deletions(-) diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml index 3deeaa7..8b8de92 100644 --- a/.github/workflows/rubocop.yml +++ b/.github/workflows/rubocop.yml @@ -13,4 +13,3 @@ jobs: uses: gimenete/rubocop-action@1.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 9a531a9..69435a4 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Ruby ${{ matrix.ruby-version }} uses: ruby/setup-ruby@v1 with: - ruby-version: ${{ matrix.ruby-version }} + ruby-version: ${{ matrix.ruby-version }} - name: Install dependencies run: bundle install - name: Init submodules diff --git a/Changelog.md b/Changelog.md index 5810c28..498d020 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,9 @@ +# Changelog + +## v3.0.0: TBD +- Exceptions reworked, top level exception is now Argon2::Error (was + Argon2::ArgonHashFail). Now supports specific errors as well, see: errors.rb + ## v2.0.3: 2021-01-02 - Address potential memory leak. Unlikely to be exploitable. diff --git a/README.md b/README.md index 4b4844b..679ad11 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,11 @@ myhash = argon.create("A password") Argon2::Password.verify_password("A password", myhash, KEY) ``` +## Version 3.0 - Migrating to the new helpers + +TODO: Write this guide once the helpers are agreed upon and finalized. Include + a note about the updated error classes as well. + ## Version 2.0 - Argon 2id Version 2.x upwards will now default to the Argon2id hash format. This is diff --git a/argon2.gemspec b/argon2.gemspec index a965d65..d3eb332 100644 --- a/argon2.gemspec +++ b/argon2.gemspec @@ -16,6 +16,13 @@ Gem::Specification.new do |spec| spec.homepage = 'https://github.com/technion/ruby-argon2' spec.license = 'MIT' + # TODO: Remove post install message after `v3.0.0` is released. + spec.post_install_message = + 'Version 3.0 of the Argon2 ruby wrapper includes breaking changes on the '\ + 'usage/generation of passwords. If you use Argon2 directly, you will need '\ + 'to follow the "migrating to v3" guide in the project README. Otherwise '\ + 'you can safely ignore this message.' + spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } spec.files << `find ext`.split diff --git a/lib/argon2/errors.rb b/lib/argon2/errors.rb index 4f90b8a..deb1c8e 100644 --- a/lib/argon2/errors.rb +++ b/lib/argon2/errors.rb @@ -2,10 +2,40 @@ module Argon2 ## - # Generic error for applications to catch when using Argon2::Engine and - # Argon2::Password + # Generic error to catch anything the Argon2 ruby library throws. # - class ArgonHashFail < StandardError; end + class Error < StandardError; end + + ## + # Various errors for invalid parameters passed to the library. + # + # WIP + # + module Errors + class InvalidHash < Argon2::Error; end + + ## + # Not used directly, but allows developers to catch any cost exception + # regardless of which cost is invalid. + # + class InvalidCost < Argon2::Error; end + + class InvalidTCost < InvalidCost; end + + class InvalidMCost < InvalidCost; end + + class InvalidPassword < Argon2::Error + def initialize(msg="Invalid password (expected a String)") + super + end + end + + class InvalidSaltSize < Argon2::Error; end + + class InvalidOutputLength < Argon2::Error; end + + class ExtError < Argon2::Error; end + end ## # Defines an array of errors that matches the enum list of errors from diff --git a/lib/argon2/ffi_engine.rb b/lib/argon2/ffi_engine.rb index be21898..9b98549 100644 --- a/lib/argon2/ffi_engine.rb +++ b/lib/argon2/ffi_engine.rb @@ -53,14 +53,14 @@ module Ext class Engine def self.hash_argon2i(password, salt, t_cost, m_cost, out_len = nil) out_len = (out_len || Constants::OUT_LEN).to_i - raise ArgonHashFail, "Invalid output length" if out_len < 1 + raise ::Argon2::Errors::InvalidOutputLength if out_len < 1 result = '' FFI::MemoryPointer.new(:char, out_len) do |buffer| ret = Ext.argon2i_hash_raw(t_cost, 1 << m_cost, 1, password, password.length, salt, salt.length, buffer, out_len) - raise ArgonHashFail, ERRORS[ret.abs] unless ret.zero? + raise ::Argon2::Errors::ExtError, ERRORS[ret.abs] unless ret.zero? result = buffer.read_string(out_len) end @@ -69,14 +69,14 @@ def self.hash_argon2i(password, salt, t_cost, m_cost, out_len = nil) def self.hash_argon2id(password, salt, t_cost, m_cost, out_len = nil) out_len = (out_len || Constants::OUT_LEN).to_i - raise ArgonHashFail, "Invalid output length" if out_len < 1 + raise ::Argon2::Errors::InvalidOutputLength if out_len < 1 result = '' FFI::MemoryPointer.new(:char, out_len) do |buffer| ret = Ext.argon2id_hash_raw(t_cost, 1 << m_cost, 1, password, password.length, salt, salt.length, buffer, out_len) - raise ArgonHashFail, ERRORS[ret.abs] unless ret.zero? + raise ::Argon2::Errors::ExtError, ERRORS[ret.abs] unless ret.zero? result = buffer.read_string(out_len) end @@ -87,13 +87,13 @@ def self.hash_argon2id_encode(password, salt, t_cost, m_cost, secret) result = '' secretlen = secret.nil? ? 0 : secret.bytesize passwordlen = password.nil? ? 0 : password.bytesize - raise ArgonHashFail, "Invalid salt size" if salt.length != Constants::SALT_LEN + raise ::Argon2::Errors::InvalidSaltSize if salt.length != Constants::SALT_LEN FFI::MemoryPointer.new(:char, Constants::ENCODE_LEN) do |buffer| ret = Ext.argon2_wrap(buffer, password, passwordlen, salt, salt.length, t_cost, (1 << m_cost), 1, secret, secretlen) - raise ArgonHashFail, ERRORS[ret.abs] unless ret.zero? + raise ::Argon2::Errors::ExtError, ERRORS[ret.abs] unless ret.zero? result = buffer.read_string(Constants::ENCODE_LEN) end @@ -106,7 +106,7 @@ def self.argon2_verify(pwd, hash, secret) ret = Ext.wrap_argon2_verify(hash, pwd, passwordlen, secret, secretlen) return false if ERRORS[ret.abs] == 'ARGON2_DECODING_FAIL' - raise ArgonHashFail, ERRORS[ret.abs] unless ret.zero? + raise ::Argon2::Errors::ExtError, ERRORS[ret.abs] unless ret.zero? true end diff --git a/lib/argon2/password.rb b/lib/argon2/password.rb index 4d92672..ceaa4e1 100644 --- a/lib/argon2/password.rb +++ b/lib/argon2/password.rb @@ -5,27 +5,27 @@ module Argon2 class Password def initialize(options = {}) @t_cost = options[:t_cost] || 2 - raise ArgonHashFail, "Invalid t_cost" if @t_cost < 1 || @t_cost > 750 + raise ::Argon2::Errors::InvalidTCost if @t_cost < 1 || @t_cost > 750 @m_cost = options[:m_cost] || 16 - raise ArgonHashFail, "Invalid m_cost" if @m_cost < 1 || @m_cost > 31 + raise ::Argon2::Errors::InvalidMCost if @m_cost < 1 || @m_cost > 31 @salt = options[:salt_do_not_supply] || Engine.saltgen @secret = options[:secret] end - def create(pass) - raise ArgonHashFail, "Invalid password (expected string)" unless - pass.is_a?(String) + def create(password) + raise ::Argon2::Errors::InvalidPassword unless password.is_a?(String) - Argon2::Engine.hash_argon2id_encode( - pass, @salt, @t_cost, @m_cost, @secret) + ::Argon2::Engine.hash_argon2id_encode( + password, @salt, @t_cost, @m_cost, @secret + ) end # Helper class, just creates defaults and calls hash() - def self.create(pass) + def self.create(password) argon2 = Argon2::Password.new - argon2.create(pass) + argon2.create(password) end # Supports 1 and argon2id formats. @@ -33,10 +33,10 @@ def self.valid_hash?(hash) /^\$argon2(id?|d).{,113}/ =~ hash end - def self.verify_password(pass, hash, secret = nil) + def self.verify_password(password, hash, secret = nil) raise ArgonHashFail, "Invalid hash" unless valid_hash?(hash) - Argon2::Engine.argon2_verify(pass, hash, secret) + ::Argon2::Engine.argon2_verify(password, hash, secret) end end end diff --git a/lib/argon2/version.rb b/lib/argon2/version.rb index 8676113..f733eac 100644 --- a/lib/argon2/version.rb +++ b/lib/argon2/version.rb @@ -4,5 +4,5 @@ module Argon2 ## # Standard Gem version constant. # - VERSION = "2.0.3" + VERSION = "3.0.0" end diff --git a/test/error_test.rb b/test/error_test.rb index be87dbb..3f8dbe6 100644 --- a/test/error_test.rb +++ b/test/error_test.rb @@ -4,26 +4,26 @@ class Argon2ErrorTest < Minitest::Test def test_ffi_fail - assert_raises Argon2::ArgonHashFail do + assert_raises Argon2::Error do Argon2::Engine.hash_argon2i("password", "somesalt\0\0\0\0\0\0\0\0", 2, 1) end end def test_memory_too_small - assert_raises Argon2::ArgonHashFail do + assert_raises Argon2::Error do Argon2::Engine.hash_argon2id_encode("password", "somesalt\0\0\0\0\0\0\0\0", 2, 1, nil) end end def test_salt_size - assert_raises Argon2::ArgonHashFail do + assert_raises Argon2::Error do Argon2::Engine.hash_argon2id_encode("password", "somesalt", 2, 16, nil) end end def test_passwd_null - assert_raises Argon2::ArgonHashFail do + assert_raises Argon2::Error do Argon2::Engine.hash_argon2id_encode(nil, "somesalt\0\0\0\0\0\0\0\0", 2, 16, nil) end From f8106d0148540a947d6018b89f2fca7f0d210620 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sat, 27 Mar 2021 12:58:51 -0700 Subject: [PATCH 05/35] Add migration to v3 notes for new errors --- README.md | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 679ad11..45b3cd5 100644 --- a/README.md +++ b/README.md @@ -98,10 +98,36 @@ myhash = argon.create("A password") Argon2::Password.verify_password("A password", myhash, KEY) ``` -## Version 3.0 - Migrating to the new helpers +## Migrating to v3.0 -TODO: Write this guide once the helpers are agreed upon and finalized. Include - a note about the updated error classes as well. +### Errors restructured + +**Root level error renamed** (find/replace all instances in your application): + +```ruby +Argon2::ArgonHashFail +``` + +Has been renamed to: + +```ruby +Argon2::Error +``` + +The following new Errors have been added for capturing specific exceptions: + +* Argon2::Errors::InvalidHash +* Argon2::Errors::InvalidCost + * Argon2::Errors::InvalidTCost + * Argon2::Errors::InvalidMCost +* Argon2::Errors::InvalidPassword +* Argon2::Errors::InvalidSaltSize +* Argon2::Errors::InvalidOutputLength +* Argon2::Errors::ExtError + +### Argon2::Engine and Argon2::Password refactored + +TODO: Write this guide once the helpers are agreed upon and finalized. ## Version 2.0 - Argon 2id From 3abc5d208b24a90e6fdbc8b78439399ae8be765a Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sat, 27 Mar 2021 13:01:52 -0700 Subject: [PATCH 06/35] Does this markdown formatting even work? --- README.md | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 45b3cd5..0de1822 100644 --- a/README.md +++ b/README.md @@ -116,14 +116,30 @@ Argon2::Error The following new Errors have been added for capturing specific exceptions: -* Argon2::Errors::InvalidHash -* Argon2::Errors::InvalidCost - * Argon2::Errors::InvalidTCost - * Argon2::Errors::InvalidMCost -* Argon2::Errors::InvalidPassword -* Argon2::Errors::InvalidSaltSize -* Argon2::Errors::InvalidOutputLength -* Argon2::Errors::ExtError +* ```ruby + Argon2::Errors::InvalidHash + ``` +* ```ruby + Argon2::Errors::InvalidCost + ``` + * ```ruby + Argon2::Errors::InvalidTCost + ``` + * ```ruby + Argon2::Errors::InvalidMCost + ``` +* ```ruby + Argon2::Errors::InvalidPassword + ``` +* ```ruby + Argon2::Errors::InvalidSaltSize + ``` +* ```ruby + Argon2::Errors::InvalidOutputLength + ``` +* ```ruby + Argon2::Errors::ExtError + ``` ### Argon2::Engine and Argon2::Password refactored From cadaf883269466383b8abf9b4616eb41944bf957 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sat, 27 Mar 2021 13:06:04 -0700 Subject: [PATCH 07/35] Keep the list styling simple --- README.md | 46 ++++++++++++++++++---------------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 0de1822..3baf5a5 100644 --- a/README.md +++ b/README.md @@ -102,44 +102,34 @@ Argon2::Password.verify_password("A password", myhash, KEY) ### Errors restructured -**Root level error renamed** (find/replace all instances in your application): +**The root level error for Argon2 has been renamed.** + +Find and replace all instances of: ```ruby Argon2::ArgonHashFail ``` -Has been renamed to: +With: ```ruby Argon2::Error ``` -The following new Errors have been added for capturing specific exceptions: - -* ```ruby - Argon2::Errors::InvalidHash - ``` -* ```ruby - Argon2::Errors::InvalidCost - ``` - * ```ruby - Argon2::Errors::InvalidTCost - ``` - * ```ruby - Argon2::Errors::InvalidMCost - ``` -* ```ruby - Argon2::Errors::InvalidPassword - ``` -* ```ruby - Argon2::Errors::InvalidSaltSize - ``` -* ```ruby - Argon2::Errors::InvalidOutputLength - ``` -* ```ruby - Argon2::Errors::ExtError - ``` +Additionally, the following new Errors have been added for capturing specific +exceptions. You aren't required to use them; they are provided purely as a way +to catch more specific exceptions as needed. + +```ruby +Argon2::Errors::InvalidHash +Argon2::Errors::InvalidCost +Argon2::Errors::InvalidTCost # Inherits from Argon2::Errors::InvalidCost +Argon2::Errors::InvalidMCost # Inherits from Argon2::Errors::InvalidCost +Argon2::Errors::InvalidPassword +Argon2::Errors::InvalidSaltSize +Argon2::Errors::InvalidOutputLength +Argon2::Errors::ExtError +``` ### Argon2::Engine and Argon2::Password refactored From 707a10dab83f47b17d72630c4b324fed96a61c05 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sat, 27 Mar 2021 22:04:52 -0700 Subject: [PATCH 08/35] Refactor the Argon2::Password class Move from Minitest to RSpec (will add additional specs soon) --- .gitignore | 3 + Gemfile | 18 ++ README.md | 8 +- Rakefile | 13 +- argon2.gemspec | 7 +- lib/argon2.rb | 2 +- lib/argon2/constants.rb | 4 +- lib/argon2/errors.rb | 9 +- lib/argon2/password.rb | 191 ++++++++++++++++-- spec/argon2/engine_spec.rb | 7 + spec/argon2/errors_spec.rb | 71 +++++++ spec/argon2/password_spec.rb | 7 + spec/minitest/README.md | 6 + spec/minitest/api_spec.rb | 51 +++++ spec/minitest/engine_spec.rb | 20 ++ .../minitest/error_spec.rb | 28 +-- spec/minitest/key_spec.rb | 32 +++ spec/minitest/low_level_spec.rb | 104 ++++++++++ spec/minitest/rubycheck_spec.rb | 40 ++++ spec/minitest/saltlen_spec.rb | 17 ++ spec/minitest/unicode_spec.rb | 28 +++ spec/spec_helper.rb | 52 +++++ test/api_test.rb | 43 ---- test/engine_test.rb | 16 -- test/key_test.rb | 21 -- test/legacy.rb | 23 --- test/low_level_test.rb | 82 -------- test/rubycheck_test.rb | 36 ---- test/saltlen_test.rb | 18 -- test/test_helper.rb | 18 -- test/unicode_test.rb | 28 --- 31 files changed, 658 insertions(+), 345 deletions(-) create mode 100644 spec/argon2/engine_spec.rb create mode 100644 spec/argon2/errors_spec.rb create mode 100644 spec/argon2/password_spec.rb create mode 100644 spec/minitest/README.md create mode 100644 spec/minitest/api_spec.rb create mode 100644 spec/minitest/engine_spec.rb rename test/error_test.rb => spec/minitest/error_spec.rb (59%) create mode 100644 spec/minitest/key_spec.rb create mode 100644 spec/minitest/low_level_spec.rb create mode 100644 spec/minitest/rubycheck_spec.rb create mode 100644 spec/minitest/saltlen_spec.rb create mode 100644 spec/minitest/unicode_spec.rb create mode 100644 spec/spec_helper.rb delete mode 100644 test/api_test.rb delete mode 100644 test/engine_test.rb delete mode 100644 test/key_test.rb delete mode 100644 test/legacy.rb delete mode 100644 test/low_level_test.rb delete mode 100644 test/rubycheck_test.rb delete mode 100644 test/saltlen_test.rb delete mode 100644 test/test_helper.rb delete mode 100644 test/unicode_test.rb diff --git a/.gitignore b/.gitignore index d2a1432..4464ba1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ /ext/argon2_wrap/tests /ext/argon2_wrap/libargon2_wrap.bundle* argon2-0.0.2.gem + +# Ignore Byebug command history file. +.byebug_history diff --git a/Gemfile b/Gemfile index 5f10ba8..0539dd9 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,22 @@ # frozen_string_literal: true source 'https://rubygems.org' + gemspec + +group :test do + # TODO: Remove/relax version locks where possible + # gem 'bundler', '~> 2.0' + # gem 'minitest', '~> 5.8' + gem 'rake', '~> 13.0.1' + gem 'rubocop', '~> 1.7' + gem 'simplecov', '~> 0.20' + gem 'simplecov-lcov', '~> 0.8' + + gem 'byebug' + gem 'faker' + gem 'rspec' + gem 'shoulda-matchers' + # gem 'simplecov', require: false + # gem 'timecop' # TODO: Unused, remove? +end diff --git a/README.md b/README.md index 3baf5a5..26befce 100644 --- a/README.md +++ b/README.md @@ -122,9 +122,11 @@ to catch more specific exceptions as needed. ```ruby Argon2::Errors::InvalidHash -Argon2::Errors::InvalidCost -Argon2::Errors::InvalidTCost # Inherits from Argon2::Errors::InvalidCost -Argon2::Errors::InvalidMCost # Inherits from Argon2::Errors::InvalidCost +Argon2::Errors::InvalidVersion # Inherits from Argon2::Errors::InvalidHash +Argon2::Errors::InvalidCost # Inherits from Argon2::Errors::InvalidHash +Argon2::Errors::InvalidTCost # Inherits from Argon2::Errors::InvalidCost +Argon2::Errors::InvalidMCost # Inherits from Argon2::Errors::InvalidCost +Argon2::Errors::InvalidPCost # Inherits from Argon2::Errors::InvalidCost Argon2::Errors::InvalidPassword Argon2::Errors::InvalidSaltSize Argon2::Errors::InvalidOutputLength diff --git a/Rakefile b/Rakefile index 98db72a..d9b6364 100644 --- a/Rakefile +++ b/Rakefile @@ -1,16 +1,9 @@ # frozen_string_literal: true require "bundler/gem_tasks" -require "rake/testtask" +require 'rspec/core/rake_task' require 'rubocop/rake_task' - +RSpec::Core::RakeTask.new(:spec) RuboCop::RakeTask.new -Rake::TestTask.new(:test) do |t| - t.libs << "test" - t.libs << "lib" - t.warning = true - t.test_files = FileList['test/**/*_test.rb'] -end - -task :default => :test +task default: [:spec] diff --git a/argon2.gemspec b/argon2.gemspec index d3eb332..3427343 100644 --- a/argon2.gemspec +++ b/argon2.gemspec @@ -29,14 +29,9 @@ Gem::Specification.new do |spec| spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] + spec.add_dependency 'ffi', '~> 1.14' spec.add_dependency 'ffi-compiler', '~> 1.0' - spec.add_development_dependency "bundler", '~> 2.0' - spec.add_development_dependency "minitest", '~> 5.8' - spec.add_development_dependency "rake", '~> 13.0.1' - spec.add_development_dependency "rubocop", '~> 1.7' - spec.add_development_dependency "simplecov", '~> 0.20' - spec.add_development_dependency "simplecov-lcov", '~> 0.8' spec.extensions << 'ext/argon2_wrap/extconf.rb' end diff --git a/lib/argon2.rb b/lib/argon2.rb index e4234da..f912647 100644 --- a/lib/argon2.rb +++ b/lib/argon2.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true ## -# This Ruby Gem provides FFI bindings, and a simplified interface, to the Argon2 +# This Ruby Gem provides FFI bindings and a simplified interface to the Argon2 # algorithm. Argon2 is the official winner of the Password Hashing Competition, # a several year project to identify a successor to bcrypt/PBKDF/scrypt methods # of securely storing passwords. This is an independent project and not official diff --git a/lib/argon2/constants.rb b/lib/argon2/constants.rb index ac62a02..3113546 100644 --- a/lib/argon2/constants.rb +++ b/lib/argon2/constants.rb @@ -6,8 +6,8 @@ module Argon2 # SALT_LEN is a standard recommendation from the Argon2 spec. # module Constants - SALT_LEN = 16 - OUT_LEN = 32 # Binary, unencoded output + SALT_LEN = 16 + OUT_LEN = 32 # Binary, unencoded output ENCODE_LEN = 108 # Encoded output end end diff --git a/lib/argon2/errors.rb b/lib/argon2/errors.rb index deb1c8e..0864421 100644 --- a/lib/argon2/errors.rb +++ b/lib/argon2/errors.rb @@ -14,16 +14,20 @@ class Error < StandardError; end module Errors class InvalidHash < Argon2::Error; end + class InvalidVersion < InvalidHash; end + ## # Not used directly, but allows developers to catch any cost exception # regardless of which cost is invalid. # - class InvalidCost < Argon2::Error; end + class InvalidCost < InvalidHash; end class InvalidTCost < InvalidCost; end class InvalidMCost < InvalidCost; end + class InvalidPCost < InvalidCost; end + class InvalidPassword < Argon2::Error def initialize(msg="Invalid password (expected a String)") super @@ -39,7 +43,8 @@ class ExtError < Argon2::Error; end ## # Defines an array of errors that matches the enum list of errors from - # argon2.h. This allows return values to propagate errors through the FFI. + # argon2.h. This allows return values to propagate errors through the FFI. Any + # error from this list will be thrown as an Argon2::Errors::ExtError # ERRORS = %w[ ARGON2_OK diff --git a/lib/argon2/password.rb b/lib/argon2/password.rb index ceaa4e1..d883b74 100644 --- a/lib/argon2/password.rb +++ b/lib/argon2/password.rb @@ -2,41 +2,188 @@ module Argon2 ## # Front-end API for the Argon2 module. # + # TODO: Find a good place to document the following knowledge, it's a PITA to + # google atm. + # + # 1) Variant used + # 2) Version used + # 3) Config used + # a) m - m_cost (memory) + # b) t - t_cost (time/stretches) + # c) p - p_cost (parallelism/cores) + # 4) Salt + # 5) Checksum + # + # | 1 | 2 | 3 | 4 | 5 (truncated) + # $argon2i$v=19$m=65536,t=2,p=1$VG9vTG9uZ1NhbGVMZW5ndGg$mYleBHsG6N0+H4JGJ0xXoI + # class Password - def initialize(options = {}) - @t_cost = options[:t_cost] || 2 - raise ::Argon2::Errors::InvalidTCost if @t_cost < 1 || @t_cost > 750 + # + DEFAULT_T_COST = 2 + DEFAULT_M_COST = 16 + # + MIN_T_COST = 1 + MAX_T_COST = 750 + # + MIN_M_COST = 1 + MAX_M_COST = 31 + # The complete Argon2 digest string (not to be confused with the checksum). + attr_reader :digest + # The hash portion of the stored password hash. + attr_reader :checksum + # The salt of the store password hash (including version and cost). + attr_reader :salt + # Variant used (argon2[i|d|id]) + attr_reader :variant + # The version of the argon2 algorithm used to create the hash. + attr_reader :version + # The time cost factor used to create the hash. + attr_reader :t_cost + # The memory cost factor used to create the hash. + attr_reader :m_cost + # The parallelism cost factor used to create the hash. + attr_reader :p_cost + + class << self + def create(password, options = {}) + raise Argon2::Errors::InvalidPassword unless password.is_a?(String) + + t_cost = options[:t_cost] || DEFAULT_T_COST + m_cost = options[:m_cost] || DEFAULT_M_COST + + if t_cost < MIN_T_COST || t_cost > MAX_T_COST + raise Argon2::Errors::InvalidTCost + end + + if m_cost < MIN_M_COST || m_cost > MAX_M_COST + raise Argon2::Errors::InvalidMCost + end + + # TODO: Add support for changing the p_cost + + salt = options[:salt_do_not_supply] || Engine.saltgen + secret = options[:secret] - @m_cost = options[:m_cost] || 16 - raise ::Argon2::Errors::InvalidMCost if @m_cost < 1 || @m_cost > 31 + Argon2::Password.new( + Argon2::Engine.hash_argon2id_encode( + password, salt, t_cost, m_cost, secret + ) + ) + end - @salt = options[:salt_do_not_supply] || Engine.saltgen - @secret = options[:secret] + # Supports 1 and argon2id formats. + def valid_hash?(digest) + /^\$argon2(id?|d).{,113}/ =~ digest + end + + # Provided for those who prefer the previous method of password comparison + def verify_password(password, digest, secret = nil) + digest = digest.to_s + if password.is_a?(Argon2::Password) + password == Argon2::Password.new(digest) + else + Argon2::Engine.argon2_verify(password, digest, secret) + end + end + end + + ## + # Initialize an Argon2::Password instance using any valid Argon2 digest. + # + def initialize(digest) + if valid_hash?(digest) + # Split the digest into its component pieces + split_digest = split_hash(digest) + # Assign each piece to the Argon2::Password instance + @digest = digest + @variant = split_digest[:variant] + @version = split_digest[:version] + @t_cost = split_digest[:t_cost] + @m_cost = split_digest[:m_cost] + @p_cost = split_digest[:p_cost] + @salt = split_digest[:salt] + @checksum = split_digest[:checksum] + else + raise Argon2::Errors::InvalidHash + end + end + + ## + # Helper function to allow easily comparing an Argon2::Password against the + # provided password and secret. + # + def matches?(password, secret = nil) + self.class.verify_password(password, digest, secret) end - def create(password) - raise ::Argon2::Errors::InvalidPassword unless password.is_a?(String) + ## + # Compares two Argon2::Password instances to see if they come from the same + # digest/hash. + # + def ==(password) + unless password.is_a?(Argon2::Password) + raise ArgumentError, + 'Can only compare an Argon2::Password against another Argon2::Password' + end - ::Argon2::Engine.hash_argon2id_encode( - password, @salt, @t_cost, @m_cost, @secret - ) + # TODO: Use secure compare to protect against timing attacks? Also, should + # this comparison be more strict? + self.digest == password.digest end - # Helper class, just creates defaults and calls hash() - def self.create(password) - argon2 = Argon2::Password.new - argon2.create(password) + ## + # Converts an Argon2::Password instance into a String. + # + def to_s + digest.to_s end - # Supports 1 and argon2id formats. - def self.valid_hash?(hash) - /^\$argon2(id?|d).{,113}/ =~ hash + ## + # Converts an Argon2::Password instance into a String. + # + def to_str + digest.to_str end - def self.verify_password(password, hash, secret = nil) - raise ArgonHashFail, "Invalid hash" unless valid_hash?(hash) + private + + ## + # Helper method to allow checking if a hash is valid in the initializer. + # + def valid_hash?(digest) + self.class.valid_hash?(digest) + end + + ## + # Helper method to extract the various values from a digest into attributes. + # + def split_hash(digest) + # TODO: Is there a better way to explode the digest into attributes? + _, variant, version, config, salt, checksum = digest.split('$') + # Regex magic to extract the values for each setting + version = /v=(\d+)/.match(version) + t_cost = /t=(\d+),/.match(config) + m_cost = /m=(\d+),/.match(config) + p_cost = /p=(\d+)/.match(config) + + # Make sure none of the values are missing + raise Argon2::Errors::InvalidVersion if version.nil? + raise Argon2::Errors::InvalidTCost if t_cost.nil? + raise Argon2::Errors::InvalidMCost if m_cost.nil? + raise Argon2::Errors::InvalidPCost if p_cost.nil? + # Undo the 2^m_cost operation when encoding the hash to get the original + # m_cost input back. + m_cost = Math.log2(m_cost[1].to_i).to_i - ::Argon2::Engine.argon2_verify(password, hash, secret) + { + variant: variant.to_str, + version: version[1].to_i, + t_cost: t_cost[1].to_i, + m_cost: m_cost, + p_cost: p_cost[1].to_i, + salt: salt.to_str, + checksum: checksum.to_str + } end end end diff --git a/spec/argon2/engine_spec.rb b/spec/argon2/engine_spec.rb new file mode 100644 index 0000000..6d84ccf --- /dev/null +++ b/spec/argon2/engine_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Argon2::Engine do + # TODO +end diff --git a/spec/argon2/errors_spec.rb b/spec/argon2/errors_spec.rb new file mode 100644 index 0000000..b1b8dea --- /dev/null +++ b/spec/argon2/errors_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Errors' do + describe Argon2::Error do + subject { described_class } + + it { is_expected.to be < StandardError } + end + + describe Argon2::Errors::InvalidHash do + subject { described_class } + + it { is_expected.to be < Argon2::Error } + end + + describe Argon2::Errors::InvalidVersion do + subject { described_class } + + it { is_expected.to be < Argon2::Errors::InvalidHash } + end + + describe Argon2::Errors::InvalidCost do + subject { described_class } + + it { is_expected.to be < Argon2::Errors::InvalidHash } + end + + describe Argon2::Errors::InvalidTCost do + subject { described_class } + + it { is_expected.to be < Argon2::Errors::InvalidCost } + end + + describe Argon2::Errors::InvalidMCost do + subject { described_class } + + it { is_expected.to be < Argon2::Errors::InvalidCost } + end + + describe Argon2::Errors::InvalidPCost do + subject { described_class } + + it { is_expected.to be < Argon2::Errors::InvalidCost } + end + + describe Argon2::Errors::InvalidPassword do + subject { described_class } + + it { is_expected.to be < Argon2::Error } + end + + describe Argon2::Errors::InvalidSaltSize do + subject { described_class } + + it { is_expected.to be < Argon2::Error } + end + + describe Argon2::Errors::InvalidOutputLength do + subject { described_class } + + it { is_expected.to be < Argon2::Error } + end + + describe Argon2::Errors::ExtError do + subject { described_class } + + it { is_expected.to be < Argon2::Error } + end +end diff --git a/spec/argon2/password_spec.rb b/spec/argon2/password_spec.rb new file mode 100644 index 0000000..530932f --- /dev/null +++ b/spec/argon2/password_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Argon2::Password do + # TODO +end diff --git a/spec/minitest/README.md b/spec/minitest/README.md new file mode 100644 index 0000000..78f3342 --- /dev/null +++ b/spec/minitest/README.md @@ -0,0 +1,6 @@ +# Minitest Replacement Specs + +Replacement specs used to prove 1:1 relationship with the previous test suite. + +Once the argon2 rspec suite is complete, and proven/trusted, this folder can be +removed. diff --git a/spec/minitest/api_spec.rb b/spec/minitest/api_spec.rb new file mode 100644 index 0000000..6e66c2f --- /dev/null +++ b/spec/minitest/api_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Argon2APITest' do + let(:original_password) { 'mypassword' } + let(:pepper) { 'A secret' } + + it 'can create a password without parameters' do + argon2 = Argon2::Password.create(original_password) + verify = Argon2::Password.verify_password(original_password, argon2) + + expect(argon2).to be_a Argon2::Password + expect(argon2.m_cost).to eq 16 + expect(argon2.t_cost).to eq 2 + expect(argon2).to be_matches original_password + + expect(verify).to be_truthy + end + + it 'can create a password with parameters' do + argon2 = Argon2::Password.create(original_password, t_cost: 4, m_cost: 12) + verify = Argon2::Password.verify_password(original_password, argon2) + + expect(argon2).to be_a Argon2::Password + expect(argon2.m_cost).to eq 12 + expect(argon2.t_cost).to eq 4 + expect(argon2).to be_matches original_password + + expect(verify).to be_truthy + end + + it 'can create a password with a secret' do + argon2 = Argon2::Password.create(original_password, secret: pepper) + verify = Argon2::Password.verify_password(original_password, argon2, pepper) + + expect(argon2).to be_a Argon2::Password + expect(argon2).to be_matches original_password, pepper + + expect(verify).to be_truthy + end + + # The test_hash test is no longer relevant, as you can no longer create an + # empty Argon2::Password instance. + + it 'can create a valid hash' do + argon2 = Argon2::Password.create(original_password) + + expect(Argon2::Password.valid_hash?(argon2)).to be_truthy + end +end diff --git a/spec/minitest/engine_spec.rb b/spec/minitest/engine_spec.rb new file mode 100644 index 0000000..2422a3b --- /dev/null +++ b/spec/minitest/engine_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'EngineTest' do + it 'generates 10 unique salts' do + salts = [] + # Generate 10 salts... + 10.times do + salts << Argon2::Engine.saltgen + end + # Check for bad salts... + duplicate_salts = salts.select{ |salt| salts.count(salt) > 1 } + wrong_length_salts = + salts.reject { |salt| salt.length == Argon2::Constants::SALT_LEN } + + expect(duplicate_salts.size).to be_zero + expect(wrong_length_salts.size).to be_zero + end +end diff --git a/test/error_test.rb b/spec/minitest/error_spec.rb similarity index 59% rename from test/error_test.rb rename to spec/minitest/error_spec.rb index 3f8dbe6..3a4851f 100644 --- a/test/error_test.rb +++ b/spec/minitest/error_spec.rb @@ -1,31 +1,31 @@ # frozen_string_literal: true -require 'test_helper' +require 'spec_helper' -class Argon2ErrorTest < Minitest::Test - def test_ffi_fail - assert_raises Argon2::Error do +describe 'Argon2ErrorTest' do + it 'ffi fail' do + expect{ Argon2::Engine.hash_argon2i("password", "somesalt\0\0\0\0\0\0\0\0", 2, 1) - end + }.to raise_error Argon2::Error end - def test_memory_too_small - assert_raises Argon2::Error do + it 'memory too small' do + expect{ Argon2::Engine.hash_argon2id_encode("password", "somesalt\0\0\0\0\0\0\0\0", 2, 1, nil) - end + }.to raise_error Argon2::Error end - def test_salt_size - assert_raises Argon2::Error do + it 'salt size' do + expect{ Argon2::Engine.hash_argon2id_encode("password", "somesalt", 2, 16, nil) - end + }.to raise_error Argon2::Error end - def test_passwd_null - assert_raises Argon2::Error do + it 'password null' do + expect{ Argon2::Engine.hash_argon2id_encode(nil, "somesalt\0\0\0\0\0\0\0\0", 2, 16, nil) - end + }.to raise_error Argon2::Error end end diff --git a/spec/minitest/key_spec.rb b/spec/minitest/key_spec.rb new file mode 100644 index 0000000..69e197e --- /dev/null +++ b/spec/minitest/key_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'LowLevelArgon2Test' do + let(:key) { 'a magic key' } + let(:pass) { 'random password' } + + it 'key hash' do + # Default hash + basehash = Argon2::Password.create(pass, t_cost: 2, m_cost: 16) + # Keyed hash + keyhash = Argon2::Password.create(pass, t_cost: 2, m_cost: 16, secret: key) + + # Isn't this test somewhat pointless? Each password will have a new salt, so + # they can never match anyway. + expect(basehash).not_to eq keyhash + + # Demonstrate problem: + salthash = Argon2::Password.create(pass, t_cost: 2, m_cost: 16) + expect(basehash).not_to eq salthash + # Prove that it's not just the `==` being broken: + expect(basehash).to eq basehash + expect(salthash).to eq salthash + expect(keyhash).to eq keyhash + + # The keyed hash - without the key + expect(Argon2::Password.verify_password(pass, keyhash)).to be_falsey + # With key + expect(Argon2::Password.verify_password(pass, keyhash, key)).to be_truthy + end +end diff --git a/spec/minitest/low_level_spec.rb b/spec/minitest/low_level_spec.rb new file mode 100644 index 0000000..b3d34d0 --- /dev/null +++ b/spec/minitest/low_level_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'LowLevelArgon2Test' do + it 'has a version number' do + expect(::Argon2::VERSION).not_to be_nil + expect(::Argon2::VERSION).to be_a String + end + + it 'ffi vector' do + expect( + Argon2::Engine.hash_argon2i("password", "somesalt\0\0\0\0\0\0\0\0", 2, 16) + ).to eq '1c7eeef9e0e969b3024722fc864a1ca9f6ca20da73f9bf3f1731881beae2039e' + + expect( + Argon2::Engine.hash_argon2i("password", "somesalt\0\0\0\0\0\0\0\0", 2, 18) + ).to eq '5c6dfd2712110cf88f1426059b01d87f8210d5368da0e7ee68586e9d4af4954b' + + expect( + Argon2::Engine.hash_argon2i("password", "somesalt\0\0\0\0\0\0\0\0", 2, 8) + ).to eq 'dfebf9d4eadd6859f4cc6a9bb20043fd9da7e1e36bdacdbb05ca569f463269f8' + + expect( + Argon2::Engine.hash_argon2i("password", "somesalt\0\0\0\0\0\0\0\0", 1, 16) + ).to eq 'fabd1ddbd86a101d326ac2abe79660202b10192925d2fd2483085df94df0c91a' + + expect( + Argon2::Engine.hash_argon2i("password", "somesalt\0\0\0\0\0\0\0\0", 4, 16) + ).to eq 'b3b4cb3d6e2c1cb1e7bffdb966ab3ceafae701d6b7789c3f1e6c6b22d82d99d5' + + expect( + Argon2::Engine.hash_argon2i("differentpassword", "somesalt\0\0\0\0\0\0\0\0", 2, 16) + ).to eq 'b2db9d7c0d1288951aec4b6e1cd3835ea29a7da2ac13e6f48554a26b127146f9' + + expect( + Argon2::Engine.hash_argon2i("password", "diffsalt\0\0\0\0\0\0\0\0", 2, 16) + ).to eq 'bb6686865f2c1093f70f543c9535f807d5b42d5dc6d71f14a4a7a291913e05e0' + + expect( + Argon2::Engine.hash_argon2i("password", "somesaltsomesalt", 2, 16, 16) + ).to eq '85d58a069b81f7606dc772810d00496d' + + expect( + Argon2::Engine.hash_argon2id("password", "somesaltsomesalt", 2, 16) + ).to eq 'fc33b78139231d34b71626bd6245c1d72efa190ad605c3d8166a72adcedfa2c2' + end + + it 'encoded hash' do + expect( + Argon2::Engine.hash_argon2id_encode("password", "somesalt\0\0\0\0\0\0\0\0", 2, 16, nil) + ).to eq '$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQAAAAAAAAAAA$syf8TzB9pvMIGtFhvRATHW1nX43iP+FLaaTXnqpyMrY' + + expect( + Argon2::Engine.hash_argon2id_encode("password", "somesalt\0\0\0\0\0\0\0\0", 2, 8, nil) + ).to eq '$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQAAAAAAAAAAA$TCsNUutWgv3lowstIasFJbdiamKiq8qPUdz2wSvQ4rw' + + expect( + Argon2::Engine.hash_argon2id_encode("password", "somesalt\0\0\0\0\0\0\0\0", 1, 16, nil) + ).to eq '$argon2id$v=19$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$b7sLmBJ4YGj/yOjMnUDWC1dvrtZr7EPdMT6zB9Fq0pk' + + expect( + Argon2::Engine.hash_argon2id_encode("differentpassword", "somesalt\0\0\0\0\0\0\0\0", 2, 16, nil) + ).to eq '$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQAAAAAAAAAAA$0bDR2fpiZutijzxlxrjLnqnCSmtG1/reR4QNcavfKLk' + + expect( + Argon2::Engine.hash_argon2id_encode("password", "diffsalt\0\0\0\0\0\0\0\0", 2, 16, nil) + ).to eq '$argon2id$v=19$m=65536,t=2,p=1$ZGlmZnNhbHQAAAAAAAAAAA$vm1qQXZQ+/MgT2Y+Go7XnxtA9dJz3wotjfg0itOgKlY' + end + + it 'verify' do + expect( + Argon2::Engine.argon2_verify("password", "$argon2i$v=19$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$+r0d29hqEB0yasKr55ZgICsQGSkl0v0kgwhd+U3wyRo", nil) + ).to be_truthy + + expect( + Argon2::Engine.argon2_verify("notword", "$argon2i$v=19$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$+r0d29hqEB0yasKr55ZgICsQGSkl0v0kgwhd+U3wyRo", nil) + ).to be_falsey + + expect( + Argon2::Engine.argon2_verify("password", "$argon2id$v=19$m=262144,t=2,p=1$c29tZXNhbHQ$eP4eyR+zqlZX1y5xCFTkw9m5GYx0L5YWwvCFvtlbLow", nil) + ).to be_truthy + + expect( + Argon2::Engine.argon2_verify("password", "$argon2id$v=19$m=262144,t=2,p=1$c29tZXNhbHQ$eP4eyR+zqlZX1y5xCFTkw9m5GYx0L5YWwvCFvtlbLok", nil) + ).to be_falsey + + expect( + Argon2::Engine.argon2_verify("password", "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc", nil) + ).to be_truthy + + expect( + Argon2::Engine.argon2_verify("notword", "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc", nil) + ).to be_falsey + + expect( + Argon2::Engine.argon2_verify("password", "$argon2d$v=19$m=65536,t=2,p=1$YzI5dFpYTmhiSFFBQUFBQUFBQUFBQQ$Jxy74cswY2mq9y+u+iJcJy8EqOp4t/C7DWDzGwGB3IM", nil) + ).to be_truthy + + expect( + Argon2::Engine.argon2_verify("notword", "$argon2d$v=19$m=65536,t=2,p=1$YzI5dFpYTmhiSFFBQUFBQUFBQUFBQQ$Jxy74cswY2mq9y+u+iJcJy8EqOp4t/C7DWDzGwGB3IM", nil) + ).to be_falsey + end +end diff --git a/spec/minitest/rubycheck_spec.rb b/spec/minitest/rubycheck_spec.rb new file mode 100644 index 0000000..c930a86 --- /dev/null +++ b/spec/minitest/rubycheck_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# This was supposed to use Rubycheck, however the current version doesn't run +# These property tests identified the NULL hash bug +describe 'Argon2PropertyTest' do + let(:run_count) { (ENV['TEST_CHECKS'] || 100).to_i } + + it 'test success' do + hashlist = {} + + run_count.times do + word = Argon2::Engine.saltgen + hashlist[word] = Argon2::Password.create(word) + end + + hashlist.each do |word, hash| + expect( + Argon2::Password.verify_password(word, hash) + ).to be_truthy + end + end + + it 'test fail' do + hashlist = {} + + run_count.times do + word = Argon2::Engine.saltgen + hashlist[word] = Argon2::Password.create(word) + end + + hashlist.each do |word, hash| + wrongword = Argon2::Engine.saltgen + expect( + Argon2::Password.verify_password(wrongword, hash) + ).to be_falsey + end + end +end diff --git a/spec/minitest/saltlen_spec.rb b/spec/minitest/saltlen_spec.rb new file mode 100644 index 0000000..8a12f02 --- /dev/null +++ b/spec/minitest/saltlen_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'SaltlenTest' do + it 'long salt' do + expect( + Argon2::Password.verify_password("password", "$argon2i$v=19$m=65536,t=2,p=1$VG9vTG9uZ1NhbGVMZW5ndGg$mYleBHsG6N0+H4JGJ0xXoIRO6rWNZwN/eQQQ8eHIDmk") + ).to be_truthy + end + + it 'short salt' do + expect( + Argon2::Password.verify_password("password", "$argon2i$v=19$m=65536,t=2,p=1$VG9vU2hvcnRTYWxlTGVu$i59ELgAm5G6J+9+oZwO+kkV48tJyocNh6bHdkj9J5lk") + ).to be_truthy + end +end diff --git a/spec/minitest/unicode_spec.rb b/spec/minitest/unicode_spec.rb new file mode 100644 index 0000000..7566a43 --- /dev/null +++ b/spec/minitest/unicode_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'UnicodeTest' do + it 'accepts utf16' do + # A string found on Google which encodes with a NULL byte + unstr = "Σὲ γνωρίζω ἀπὸ τὴν κό".encode("utf-16le") + argon2 = Argon2::Password.create(unstr) + expect(Argon2::Password.verify_password(unstr, argon2)).to be_truthy + end + + it 'accepts null byte' do + rawstr = "String has a\0NULL in it" + argon2 = Argon2::Password.create(rawstr) + expect(Argon2::Password.verify_password(rawstr, argon2)).to be_truthy + # Asserts that no NULL byte truncation occurs + expect(Argon2::Password.verify_password("String has a", argon2)).to be_falsey + end + + it 'accepts emoji 😄' do + rawstr = "😀 😬 😁 😂 😃 😄 💩 😈 👿" + argon2 = Argon2::Password.create(rawstr) + expect(Argon2::Password.verify_password(rawstr, argon2)).to be_truthy + expect(Argon2::Password.verify_password("", argon2)).to be_falsey + expect(Argon2::Password.verify_password(" ", argon2)).to be_falsey + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..08030d0 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '../lib')) +$LOAD_PATH.unshift(File.dirname(__FILE__)) + +require 'byebug' +require 'faker' + +############################ +## Generate Test Coverage ## +############################ + +unless ENV['coverage'] == 'false' + require 'simplecov' + require 'simplecov-lcov' + SimpleCov::Formatter::LcovFormatter.config do |c| + c.report_with_single_file = true + c.output_directory = 'coverage/lvoc' + c.lcov_file_name = 'lcov.info' + c.single_report_path = 'coverage/lcov.info' + end + SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter + SimpleCov.start +end + +##################### +## Configure RSpec ## +##################### + +RSpec.configure do |config| + # These two settings work together to allow you to limit a spec run + # to individual examples or groups you care about by tagging them with + # `:focus` metadata. When nothing is tagged with `:focus`, all examples + # get run. + config.filter_run :focus + config.run_all_when_everything_filtered = true + + # Find load order dependencies + config.order = :random + # Allow replicating load order dependency + # by passing in same seed using --seed + Kernel.srand config.seed + + # Find slow specs by running `profiling=true rspec` + config.profile_examples = 5 if ENV['profiling'] == 'true' +end + +###################### +## Load the Library ## +###################### + +require 'argon2' diff --git a/test/api_test.rb b/test/api_test.rb deleted file mode 100644 index cd299a2..0000000 --- a/test/api_test.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require 'test_helper' - -module Argon2 - # Simple stub to facilitate testing these variables - class Password - attr_accessor :t_cost, :m_cost, :secret - end -end - -class Argon2APITest < Minitest::Test - def test_create_default - assert pass = Argon2::Password.new - assert_instance_of Argon2::Password, pass - assert_equal 16, pass.m_cost - assert_equal 2, pass.t_cost - assert_nil pass.secret - end - - def test_create_args - assert pass = Argon2::Password.new(t_cost: 4, m_cost: 12) - assert_instance_of Argon2::Password, pass - assert_equal 12, pass.m_cost - assert_equal 4, pass.t_cost - assert_nil pass.secret - end - - def test_secret - assert pass = Argon2::Password.new(secret: "A secret") - assert_equal pass.secret, "A secret" - end - - def test_hash - assert pass = Argon2::Password.new - assert pass.create('mypassword') - end - - def test_valid_hash - secure_pass = Argon2::Password.create('A secret') - assert Argon2::Password.valid_hash?(secure_pass) - end -end diff --git a/test/engine_test.rb b/test/engine_test.rb deleted file mode 100644 index cc90c6b..0000000 --- a/test/engine_test.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require 'test_helper' - -class EngineTest < Minitest::Test - def test_saltgen - generate = [] - assert(10.times { generate << Argon2::Engine.saltgen }) - duplicates = generate.select { |e| generate.count(e) > 1 } - assert_equal duplicates.length, 0 - wrong_length = generate.reject do |e| - e.length == Argon2::Constants::SALT_LEN - end - assert_equal wrong_length.size, 0 - end -end diff --git a/test/key_test.rb b/test/key_test.rb deleted file mode 100644 index 2360997..0000000 --- a/test/key_test.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require 'test_helper' - -class LowLevelArgon2Test < Minitest::Test - KEY = "a magic key" - PASS = "random password" - def test_key_hash - # Default hash - argon = Argon2::Password.new(t_cost: 2, m_cost: 16) - assert basehash = argon.create(PASS) - # Keyed hash - argon = Argon2::Password.new(t_cost: 2, m_cost: 16, secret: KEY) - assert keyhash = argon.create(PASS) - refute_equal basehash, keyhash - # The keyed hash - without the key - refute Argon2::Password.verify_password(PASS, keyhash) - # With key - assert Argon2::Password.verify_password(PASS, keyhash, KEY) - end -end diff --git a/test/legacy.rb b/test/legacy.rb deleted file mode 100644 index 455a94c..0000000 --- a/test/legacy.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'test_helper' -# frozen_string_literal: true - -class Legacy < Minitest::Test - HASH_1_0 = "$argon2i$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$+r0d29hqEB0yasKr55ZgICsQGSkl0v0kgwhd+U3wyRo" - HASH_1_1 = "$argon2i$v=19$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$+r0d29hqEB0yasKr55ZgICsQGSkl0v0kgwhd+U3wyRo" - HASH_0 = "$argon2i$v=16$m=256,t=2,p=1$c29tZXNhbHQ$/U3YPXYsSb3q9XxHvc0MLxur+GP960kN9j7emXX8zwY" - def test_legacy_hashes - # These are the hash formats for 1.0 and 1.1 of this gem. - assert Argon2::Password.verify_password(PASS, HASH_1_0) - assert Argon2::Password.verify_password(PASS, HASH_1_1) - assert Argon2::Password.verify_password(PASS, HASH_0) - end - - def test_valid_hash_legacy_hashes - # These are the hash formats for 1.0 and 1.1 of this gem. - assert Argon2::Password.valid_hash?(PASS, HASH_1_0) - assert Argon2::Password.valid_hash?(PASS, HASH_1_1) - assert Argon2::Password.valid_hash?(PASS, HASH_0) - end -end diff --git a/test/low_level_test.rb b/test/low_level_test.rb deleted file mode 100644 index d62eead..0000000 --- a/test/low_level_test.rb +++ /dev/null @@ -1,82 +0,0 @@ -# frozen_string_literal: true - -require 'test_helper' - -# Vectors in this suite taken from test.c in reference - -class LowLevelArgon2Test < Minitest::Test - def test_that_it_has_a_version_number - refute_nil ::Argon2::VERSION - end - - def test_ffi_vector - assert_equal Argon2::Engine.hash_argon2i( - "password", "somesalt\0\0\0\0\0\0\0\0", 2, 16), - '1c7eeef9e0e969b3024722fc864a1ca9f6ca20da73f9bf3f1731881beae2039e' - - assert_equal Argon2::Engine.hash_argon2i( - "password", "somesalt\0\0\0\0\0\0\0\0", 2, 18), - '5c6dfd2712110cf88f1426059b01d87f8210d5368da0e7ee68586e9d4af4954b' - - assert_equal Argon2::Engine.hash_argon2i( - "password", "somesalt\0\0\0\0\0\0\0\0", 2, 8), - 'dfebf9d4eadd6859f4cc6a9bb20043fd9da7e1e36bdacdbb05ca569f463269f8' - - assert_equal Argon2::Engine.hash_argon2i( - "password", "somesalt\0\0\0\0\0\0\0\0", 1, 16), - 'fabd1ddbd86a101d326ac2abe79660202b10192925d2fd2483085df94df0c91a' - - assert_equal Argon2::Engine.hash_argon2i( - "password", "somesalt\0\0\0\0\0\0\0\0", 4, 16), - 'b3b4cb3d6e2c1cb1e7bffdb966ab3ceafae701d6b7789c3f1e6c6b22d82d99d5' - - assert_equal Argon2::Engine.hash_argon2i( - "differentpassword", "somesalt\0\0\0\0\0\0\0\0", 2, 16), - 'b2db9d7c0d1288951aec4b6e1cd3835ea29a7da2ac13e6f48554a26b127146f9' - - assert_equal Argon2::Engine.hash_argon2i( - "password", "diffsalt\0\0\0\0\0\0\0\0", 2, 16), - 'bb6686865f2c1093f70f543c9535f807d5b42d5dc6d71f14a4a7a291913e05e0' - - assert_equal Argon2::Engine.hash_argon2i( - "password", "somesaltsomesalt", 2, 16, 16), - '85d58a069b81f7606dc772810d00496d' - - assert_equal Argon2::Engine.hash_argon2id( - "password", "somesaltsomesalt", 2, 16), - 'fc33b78139231d34b71626bd6245c1d72efa190ad605c3d8166a72adcedfa2c2' - end - - def test_encoded_hash - assert_equal Argon2::Engine.hash_argon2id_encode( - "password", "somesalt\0\0\0\0\0\0\0\0", 2, 16, nil), - '$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQAAAAAAAAAAA$syf8TzB9pvMIGtFhvRATHW1nX43iP+FLaaTXnqpyMrY' - - assert_equal Argon2::Engine.hash_argon2id_encode( - "password", "somesalt\0\0\0\0\0\0\0\0", 2, 8, nil), - '$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQAAAAAAAAAAA$TCsNUutWgv3lowstIasFJbdiamKiq8qPUdz2wSvQ4rw' - - assert_equal Argon2::Engine.hash_argon2id_encode( - "password", "somesalt\0\0\0\0\0\0\0\0", 1, 16, nil), - '$argon2id$v=19$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$b7sLmBJ4YGj/yOjMnUDWC1dvrtZr7EPdMT6zB9Fq0pk' - - assert_equal Argon2::Engine.hash_argon2id_encode( - "differentpassword", "somesalt\0\0\0\0\0\0\0\0", 2, 16, nil), - '$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQAAAAAAAAAAA$0bDR2fpiZutijzxlxrjLnqnCSmtG1/reR4QNcavfKLk' - - assert_equal Argon2::Engine.hash_argon2id_encode( - "password", "diffsalt\0\0\0\0\0\0\0\0", 2, 16, nil), - '$argon2id$v=19$m=65536,t=2,p=1$ZGlmZnNhbHQAAAAAAAAAAA$vm1qQXZQ+/MgT2Y+Go7XnxtA9dJz3wotjfg0itOgKlY' - end - - def test_verify - assert Argon2::Engine.argon2_verify("password", "$argon2i$v=19$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$+r0d29hqEB0yasKr55ZgICsQGSkl0v0kgwhd+U3wyRo", nil) - refute Argon2::Engine.argon2_verify("notword", "$argon2i$v=19$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$+r0d29hqEB0yasKr55ZgICsQGSkl0v0kgwhd+U3wyRo", nil) - assert Argon2::Engine.argon2_verify("password", "$argon2id$v=19$m=262144,t=2,p=1$c29tZXNhbHQ$eP4eyR+zqlZX1y5xCFTkw9m5GYx0L5YWwvCFvtlbLow", nil) - refute Argon2::Engine.argon2_verify("password", "$argon2id$v=19$m=262144,t=2,p=1$c29tZXNhbHQ$eP4eyR+zqlZX1y5xCFTkw9m5GYx0L5YWwvCFvtlbLok", nil) - assert Argon2::Engine.argon2_verify("password", "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc", nil) - refute Argon2::Engine.argon2_verify("notword", "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc", nil) - assert Argon2::Engine.argon2_verify("password", "$argon2d$v=19$m=65536,t=2,p=1$YzI5dFpYTmhiSFFBQUFBQUFBQUFBQQ$Jxy74cswY2mq9y+u+iJcJy8EqOp4t/C7DWDzGwGB3IM", nil) - refute Argon2::Engine.argon2_verify("notword", "$argon2d$v=19$m=65536,t=2,p=1$YzI5dFpYTmhiSFFBQUFBQUFBQUFBQQ$Jxy74cswY2mq9y+u+iJcJy8EqOp4t/C7DWDzGwGB3IM", nil) - end -end diff --git a/test/rubycheck_test.rb b/test/rubycheck_test.rb deleted file mode 100644 index 3b351c3..0000000 --- a/test/rubycheck_test.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require 'test_helper' - -TIMES = (ENV['TEST_CHECKS'] || 100).to_i - -# This was supposed to use Rubycheck, however the current version doesn't run -# These property tests identified the NULL hash bug -class Argon2PropertyTest < Minitest::Test - def test_success - hashlist = {} - TIMES.times do - word = Argon2::Engine.saltgen - assert hashlist[word] = Argon2::Password.create(word), - word.unpack('H*').join - end - hashlist.each do |word, hash| - assert Argon2::Password.verify_password(word, hash), - word.unpack('H*').join - end - end - - def test_fail - hashlist = {} - TIMES.times do - word = Argon2::Engine.saltgen - assert hashlist[word] = Argon2::Password.create(word), - word.unpack('H*').join - end - hashlist.each do |word, hash| - wrongword = Argon2::Engine.saltgen - refute Argon2::Password.verify_password(wrongword, hash), - word.unpack('H*').join - end - end -end diff --git a/test/saltlen_test.rb b/test/saltlen_test.rb deleted file mode 100644 index 397bc92..0000000 --- a/test/saltlen_test.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require 'test_helper' - -class SaltlenTest < Minitest::Test - # Tests relevant to issue #14 - def test_longsalt - # A test for verifying existing hashes with diffferent salt lengths - assert Argon2::Password.verify_password("password", - "$argon2i$v=19$m=65536,t=2,p=1$VG9vTG9uZ1NhbGVMZW5ndGg$mYleBHsG6N0+H4JGJ0xXoIRO6rWNZwN/eQQQ8eHIDmk") - end - - def test_shortsalt - # Asserts that no NULL byte truncation occurs - assert Argon2::Password.verify_password("password", - "$argon2i$v=19$m=65536,t=2,p=1$VG9vU2hvcnRTYWxlTGVu$i59ELgAm5G6J+9+oZwO+kkV48tJyocNh6bHdkj9J5lk") - end -end diff --git a/test/test_helper.rb b/test/test_helper.rb deleted file mode 100644 index c5490da..0000000 --- a/test/test_helper.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -$LOAD_PATH.unshift File.expand_path('../lib', __dir__) - -require 'simplecov' -require 'simplecov-lcov' -SimpleCov::Formatter::LcovFormatter.config do |c| - c.report_with_single_file = true - c.output_directory = 'coverage/lvoc' - c.lcov_file_name = 'lcov.info' - c.single_report_path = 'coverage/lcov.info' -end -SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter -SimpleCov.start - -require 'argon2' - -require 'minitest/autorun' diff --git a/test/unicode_test.rb b/test/unicode_test.rb deleted file mode 100644 index d406280..0000000 --- a/test/unicode_test.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -require 'test_helper' - -class UnicodeTest < Minitest::Test - def test_utf16 - # A string found on Google which encodes with a NULL byte - unstr = "Σὲ γνωρίζω ἀπὸ τὴν κό".encode("utf-16le") - assert hash = Argon2::Password.create(unstr) - assert Argon2::Password.verify_password(unstr, hash) - end - - def test_null_byte - rawstr = "String has a\0NULL in it" - hash = Argon2::Password.create(rawstr) - assert Argon2::Password.verify_password(rawstr, hash) - # Asserts that no NULL byte truncation occurs - refute Argon2::Password.verify_password("String has a", hash), - "Does not NULL truncate" - end - - def test_emoji - rawstr = "😀 😬 😁 😂 😃 😄 💩 😈 👿" - hash = Argon2::Password.create(rawstr) - assert Argon2::Password.verify_password(rawstr, hash) - refute Argon2::Password.verify_password("", hash) - end -end From bb0bdf0a9e9f5f0c94110b42a6e2432983f99a6d Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sat, 27 Mar 2021 22:59:25 -0700 Subject: [PATCH 09/35] Update Changelog.md with migration details --- Changelog.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Changelog.md b/Changelog.md index 498d020..ccc394a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,8 +1,14 @@ # Changelog ## v3.0.0: TBD -- Exceptions reworked, top level exception is now Argon2::Error (was - Argon2::ArgonHashFail). Now supports specific errors as well, see: errors.rb + +**Includes multiple breaking changes, see [README.md](README.md) for migration +instructions.** + +- Refactored Argon2::Password to include additional helpers and simplify hash + creation. +- Renamed top level exception from: `Argon2::ArgonHashHail` to: `Argon2::Error` +- Added new exceptions that inherit from the top level exception. ## v2.0.3: 2021-01-02 - Address potential memory leak. Unlikely to be exploitable. From a2a9954016592b686f32c4d0c73872974bfab4b5 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sat, 27 Mar 2021 23:59:06 -0700 Subject: [PATCH 10/35] Fix Argon2::Password.new breaking when passed another Argon2::Password --- lib/argon2/password.rb | 2 ++ spec/argon2/password_spec.rb | 14 +++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/argon2/password.rb b/lib/argon2/password.rb index d883b74..df52bfc 100644 --- a/lib/argon2/password.rb +++ b/lib/argon2/password.rb @@ -91,6 +91,7 @@ def verify_password(password, digest, secret = nil) # Initialize an Argon2::Password instance using any valid Argon2 digest. # def initialize(digest) + digest = digest.to_s if valid_hash?(digest) # Split the digest into its component pieces split_digest = split_hash(digest) @@ -121,6 +122,7 @@ def matches?(password, secret = nil) # digest/hash. # def ==(password) + # TODO: Should this return false instead of raising an error? unless password.is_a?(Argon2::Password) raise ArgumentError, 'Can only compare an Argon2::Password against another Argon2::Password' diff --git a/spec/argon2/password_spec.rb b/spec/argon2/password_spec.rb index 530932f..bcfbda1 100644 --- a/spec/argon2/password_spec.rb +++ b/spec/argon2/password_spec.rb @@ -3,5 +3,17 @@ require 'spec_helper' describe Argon2::Password do - # TODO + let(:original_password) { Faker::Internet.password } + + describe 'initialize' do + let(:digest) { Argon2::Password.create(original_password) } + + it 'accepts a valid Argon2 String' do + expect(described_class.new(digest.to_s)).to be_a described_class + end + + it 'accepts a valid Argon2::Password' do + expect(described_class.new(digest)).to be_a described_class + end + end end From 4ed16f29c73bf621d74e5d14d728a2c31bbd8116 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sun, 28 Mar 2021 07:31:26 -0700 Subject: [PATCH 11/35] Revert all test suite changes --- Gemfile | 14 +-- Rakefile | 13 ++- spec/argon2/engine_spec.rb | 7 -- spec/argon2/errors_spec.rb | 71 ------------ spec/argon2/password_spec.rb | 19 ---- spec/minitest/README.md | 6 - spec/minitest/api_spec.rb | 51 --------- spec/minitest/engine_spec.rb | 20 ---- spec/minitest/key_spec.rb | 32 ------ spec/minitest/low_level_spec.rb | 104 ------------------ spec/minitest/rubycheck_spec.rb | 40 ------- spec/minitest/saltlen_spec.rb | 17 --- spec/minitest/unicode_spec.rb | 28 ----- spec/spec_helper.rb | 52 --------- test/api_test.rb | 43 ++++++++ test/engine_test.rb | 16 +++ .../error_spec.rb => test/error_test.rb | 28 ++--- test/key_test.rb | 21 ++++ test/legacy.rb | 23 ++++ test/low_level_test.rb | 82 ++++++++++++++ test/rubycheck_test.rb | 36 ++++++ test/saltlen_test.rb | 18 +++ test/test_helper.rb | 18 +++ test/unicode_test.rb | 28 +++++ 24 files changed, 316 insertions(+), 471 deletions(-) delete mode 100644 spec/argon2/engine_spec.rb delete mode 100644 spec/argon2/errors_spec.rb delete mode 100644 spec/argon2/password_spec.rb delete mode 100644 spec/minitest/README.md delete mode 100644 spec/minitest/api_spec.rb delete mode 100644 spec/minitest/engine_spec.rb delete mode 100644 spec/minitest/key_spec.rb delete mode 100644 spec/minitest/low_level_spec.rb delete mode 100644 spec/minitest/rubycheck_spec.rb delete mode 100644 spec/minitest/saltlen_spec.rb delete mode 100644 spec/minitest/unicode_spec.rb delete mode 100644 spec/spec_helper.rb create mode 100644 test/api_test.rb create mode 100644 test/engine_test.rb rename spec/minitest/error_spec.rb => test/error_test.rb (57%) create mode 100644 test/key_test.rb create mode 100644 test/legacy.rb create mode 100644 test/low_level_test.rb create mode 100644 test/rubycheck_test.rb create mode 100644 test/saltlen_test.rb create mode 100644 test/test_helper.rb create mode 100644 test/unicode_test.rb diff --git a/Gemfile b/Gemfile index 0539dd9..d491e9f 100644 --- a/Gemfile +++ b/Gemfile @@ -4,19 +4,19 @@ source 'https://rubygems.org' gemspec +# TODO: Remove/relax version locks where possible group :test do - # TODO: Remove/relax version locks where possible - # gem 'bundler', '~> 2.0' - # gem 'minitest', '~> 5.8' + gem 'bundler', '~> 2.0' + gem 'minitest', '~> 5.8' gem 'rake', '~> 13.0.1' gem 'rubocop', '~> 1.7' gem 'simplecov', '~> 0.20' gem 'simplecov-lcov', '~> 0.8' - gem 'byebug' - gem 'faker' - gem 'rspec' - gem 'shoulda-matchers' + # gem 'byebug' + # gem 'faker' + # gem 'rspec' + # gem 'shoulda-matchers' # gem 'simplecov', require: false # gem 'timecop' # TODO: Unused, remove? end diff --git a/Rakefile b/Rakefile index d9b6364..98db72a 100644 --- a/Rakefile +++ b/Rakefile @@ -1,9 +1,16 @@ # frozen_string_literal: true require "bundler/gem_tasks" -require 'rspec/core/rake_task' +require "rake/testtask" require 'rubocop/rake_task' -RSpec::Core::RakeTask.new(:spec) + RuboCop::RakeTask.new -task default: [:spec] +Rake::TestTask.new(:test) do |t| + t.libs << "test" + t.libs << "lib" + t.warning = true + t.test_files = FileList['test/**/*_test.rb'] +end + +task :default => :test diff --git a/spec/argon2/engine_spec.rb b/spec/argon2/engine_spec.rb deleted file mode 100644 index 6d84ccf..0000000 --- a/spec/argon2/engine_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Argon2::Engine do - # TODO -end diff --git a/spec/argon2/errors_spec.rb b/spec/argon2/errors_spec.rb deleted file mode 100644 index b1b8dea..0000000 --- a/spec/argon2/errors_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe 'Errors' do - describe Argon2::Error do - subject { described_class } - - it { is_expected.to be < StandardError } - end - - describe Argon2::Errors::InvalidHash do - subject { described_class } - - it { is_expected.to be < Argon2::Error } - end - - describe Argon2::Errors::InvalidVersion do - subject { described_class } - - it { is_expected.to be < Argon2::Errors::InvalidHash } - end - - describe Argon2::Errors::InvalidCost do - subject { described_class } - - it { is_expected.to be < Argon2::Errors::InvalidHash } - end - - describe Argon2::Errors::InvalidTCost do - subject { described_class } - - it { is_expected.to be < Argon2::Errors::InvalidCost } - end - - describe Argon2::Errors::InvalidMCost do - subject { described_class } - - it { is_expected.to be < Argon2::Errors::InvalidCost } - end - - describe Argon2::Errors::InvalidPCost do - subject { described_class } - - it { is_expected.to be < Argon2::Errors::InvalidCost } - end - - describe Argon2::Errors::InvalidPassword do - subject { described_class } - - it { is_expected.to be < Argon2::Error } - end - - describe Argon2::Errors::InvalidSaltSize do - subject { described_class } - - it { is_expected.to be < Argon2::Error } - end - - describe Argon2::Errors::InvalidOutputLength do - subject { described_class } - - it { is_expected.to be < Argon2::Error } - end - - describe Argon2::Errors::ExtError do - subject { described_class } - - it { is_expected.to be < Argon2::Error } - end -end diff --git a/spec/argon2/password_spec.rb b/spec/argon2/password_spec.rb deleted file mode 100644 index bcfbda1..0000000 --- a/spec/argon2/password_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Argon2::Password do - let(:original_password) { Faker::Internet.password } - - describe 'initialize' do - let(:digest) { Argon2::Password.create(original_password) } - - it 'accepts a valid Argon2 String' do - expect(described_class.new(digest.to_s)).to be_a described_class - end - - it 'accepts a valid Argon2::Password' do - expect(described_class.new(digest)).to be_a described_class - end - end -end diff --git a/spec/minitest/README.md b/spec/minitest/README.md deleted file mode 100644 index 78f3342..0000000 --- a/spec/minitest/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Minitest Replacement Specs - -Replacement specs used to prove 1:1 relationship with the previous test suite. - -Once the argon2 rspec suite is complete, and proven/trusted, this folder can be -removed. diff --git a/spec/minitest/api_spec.rb b/spec/minitest/api_spec.rb deleted file mode 100644 index 6e66c2f..0000000 --- a/spec/minitest/api_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe 'Argon2APITest' do - let(:original_password) { 'mypassword' } - let(:pepper) { 'A secret' } - - it 'can create a password without parameters' do - argon2 = Argon2::Password.create(original_password) - verify = Argon2::Password.verify_password(original_password, argon2) - - expect(argon2).to be_a Argon2::Password - expect(argon2.m_cost).to eq 16 - expect(argon2.t_cost).to eq 2 - expect(argon2).to be_matches original_password - - expect(verify).to be_truthy - end - - it 'can create a password with parameters' do - argon2 = Argon2::Password.create(original_password, t_cost: 4, m_cost: 12) - verify = Argon2::Password.verify_password(original_password, argon2) - - expect(argon2).to be_a Argon2::Password - expect(argon2.m_cost).to eq 12 - expect(argon2.t_cost).to eq 4 - expect(argon2).to be_matches original_password - - expect(verify).to be_truthy - end - - it 'can create a password with a secret' do - argon2 = Argon2::Password.create(original_password, secret: pepper) - verify = Argon2::Password.verify_password(original_password, argon2, pepper) - - expect(argon2).to be_a Argon2::Password - expect(argon2).to be_matches original_password, pepper - - expect(verify).to be_truthy - end - - # The test_hash test is no longer relevant, as you can no longer create an - # empty Argon2::Password instance. - - it 'can create a valid hash' do - argon2 = Argon2::Password.create(original_password) - - expect(Argon2::Password.valid_hash?(argon2)).to be_truthy - end -end diff --git a/spec/minitest/engine_spec.rb b/spec/minitest/engine_spec.rb deleted file mode 100644 index 2422a3b..0000000 --- a/spec/minitest/engine_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe 'EngineTest' do - it 'generates 10 unique salts' do - salts = [] - # Generate 10 salts... - 10.times do - salts << Argon2::Engine.saltgen - end - # Check for bad salts... - duplicate_salts = salts.select{ |salt| salts.count(salt) > 1 } - wrong_length_salts = - salts.reject { |salt| salt.length == Argon2::Constants::SALT_LEN } - - expect(duplicate_salts.size).to be_zero - expect(wrong_length_salts.size).to be_zero - end -end diff --git a/spec/minitest/key_spec.rb b/spec/minitest/key_spec.rb deleted file mode 100644 index 69e197e..0000000 --- a/spec/minitest/key_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe 'LowLevelArgon2Test' do - let(:key) { 'a magic key' } - let(:pass) { 'random password' } - - it 'key hash' do - # Default hash - basehash = Argon2::Password.create(pass, t_cost: 2, m_cost: 16) - # Keyed hash - keyhash = Argon2::Password.create(pass, t_cost: 2, m_cost: 16, secret: key) - - # Isn't this test somewhat pointless? Each password will have a new salt, so - # they can never match anyway. - expect(basehash).not_to eq keyhash - - # Demonstrate problem: - salthash = Argon2::Password.create(pass, t_cost: 2, m_cost: 16) - expect(basehash).not_to eq salthash - # Prove that it's not just the `==` being broken: - expect(basehash).to eq basehash - expect(salthash).to eq salthash - expect(keyhash).to eq keyhash - - # The keyed hash - without the key - expect(Argon2::Password.verify_password(pass, keyhash)).to be_falsey - # With key - expect(Argon2::Password.verify_password(pass, keyhash, key)).to be_truthy - end -end diff --git a/spec/minitest/low_level_spec.rb b/spec/minitest/low_level_spec.rb deleted file mode 100644 index b3d34d0..0000000 --- a/spec/minitest/low_level_spec.rb +++ /dev/null @@ -1,104 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe 'LowLevelArgon2Test' do - it 'has a version number' do - expect(::Argon2::VERSION).not_to be_nil - expect(::Argon2::VERSION).to be_a String - end - - it 'ffi vector' do - expect( - Argon2::Engine.hash_argon2i("password", "somesalt\0\0\0\0\0\0\0\0", 2, 16) - ).to eq '1c7eeef9e0e969b3024722fc864a1ca9f6ca20da73f9bf3f1731881beae2039e' - - expect( - Argon2::Engine.hash_argon2i("password", "somesalt\0\0\0\0\0\0\0\0", 2, 18) - ).to eq '5c6dfd2712110cf88f1426059b01d87f8210d5368da0e7ee68586e9d4af4954b' - - expect( - Argon2::Engine.hash_argon2i("password", "somesalt\0\0\0\0\0\0\0\0", 2, 8) - ).to eq 'dfebf9d4eadd6859f4cc6a9bb20043fd9da7e1e36bdacdbb05ca569f463269f8' - - expect( - Argon2::Engine.hash_argon2i("password", "somesalt\0\0\0\0\0\0\0\0", 1, 16) - ).to eq 'fabd1ddbd86a101d326ac2abe79660202b10192925d2fd2483085df94df0c91a' - - expect( - Argon2::Engine.hash_argon2i("password", "somesalt\0\0\0\0\0\0\0\0", 4, 16) - ).to eq 'b3b4cb3d6e2c1cb1e7bffdb966ab3ceafae701d6b7789c3f1e6c6b22d82d99d5' - - expect( - Argon2::Engine.hash_argon2i("differentpassword", "somesalt\0\0\0\0\0\0\0\0", 2, 16) - ).to eq 'b2db9d7c0d1288951aec4b6e1cd3835ea29a7da2ac13e6f48554a26b127146f9' - - expect( - Argon2::Engine.hash_argon2i("password", "diffsalt\0\0\0\0\0\0\0\0", 2, 16) - ).to eq 'bb6686865f2c1093f70f543c9535f807d5b42d5dc6d71f14a4a7a291913e05e0' - - expect( - Argon2::Engine.hash_argon2i("password", "somesaltsomesalt", 2, 16, 16) - ).to eq '85d58a069b81f7606dc772810d00496d' - - expect( - Argon2::Engine.hash_argon2id("password", "somesaltsomesalt", 2, 16) - ).to eq 'fc33b78139231d34b71626bd6245c1d72efa190ad605c3d8166a72adcedfa2c2' - end - - it 'encoded hash' do - expect( - Argon2::Engine.hash_argon2id_encode("password", "somesalt\0\0\0\0\0\0\0\0", 2, 16, nil) - ).to eq '$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQAAAAAAAAAAA$syf8TzB9pvMIGtFhvRATHW1nX43iP+FLaaTXnqpyMrY' - - expect( - Argon2::Engine.hash_argon2id_encode("password", "somesalt\0\0\0\0\0\0\0\0", 2, 8, nil) - ).to eq '$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQAAAAAAAAAAA$TCsNUutWgv3lowstIasFJbdiamKiq8qPUdz2wSvQ4rw' - - expect( - Argon2::Engine.hash_argon2id_encode("password", "somesalt\0\0\0\0\0\0\0\0", 1, 16, nil) - ).to eq '$argon2id$v=19$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$b7sLmBJ4YGj/yOjMnUDWC1dvrtZr7EPdMT6zB9Fq0pk' - - expect( - Argon2::Engine.hash_argon2id_encode("differentpassword", "somesalt\0\0\0\0\0\0\0\0", 2, 16, nil) - ).to eq '$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQAAAAAAAAAAA$0bDR2fpiZutijzxlxrjLnqnCSmtG1/reR4QNcavfKLk' - - expect( - Argon2::Engine.hash_argon2id_encode("password", "diffsalt\0\0\0\0\0\0\0\0", 2, 16, nil) - ).to eq '$argon2id$v=19$m=65536,t=2,p=1$ZGlmZnNhbHQAAAAAAAAAAA$vm1qQXZQ+/MgT2Y+Go7XnxtA9dJz3wotjfg0itOgKlY' - end - - it 'verify' do - expect( - Argon2::Engine.argon2_verify("password", "$argon2i$v=19$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$+r0d29hqEB0yasKr55ZgICsQGSkl0v0kgwhd+U3wyRo", nil) - ).to be_truthy - - expect( - Argon2::Engine.argon2_verify("notword", "$argon2i$v=19$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$+r0d29hqEB0yasKr55ZgICsQGSkl0v0kgwhd+U3wyRo", nil) - ).to be_falsey - - expect( - Argon2::Engine.argon2_verify("password", "$argon2id$v=19$m=262144,t=2,p=1$c29tZXNhbHQ$eP4eyR+zqlZX1y5xCFTkw9m5GYx0L5YWwvCFvtlbLow", nil) - ).to be_truthy - - expect( - Argon2::Engine.argon2_verify("password", "$argon2id$v=19$m=262144,t=2,p=1$c29tZXNhbHQ$eP4eyR+zqlZX1y5xCFTkw9m5GYx0L5YWwvCFvtlbLok", nil) - ).to be_falsey - - expect( - Argon2::Engine.argon2_verify("password", "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc", nil) - ).to be_truthy - - expect( - Argon2::Engine.argon2_verify("notword", "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc", nil) - ).to be_falsey - - expect( - Argon2::Engine.argon2_verify("password", "$argon2d$v=19$m=65536,t=2,p=1$YzI5dFpYTmhiSFFBQUFBQUFBQUFBQQ$Jxy74cswY2mq9y+u+iJcJy8EqOp4t/C7DWDzGwGB3IM", nil) - ).to be_truthy - - expect( - Argon2::Engine.argon2_verify("notword", "$argon2d$v=19$m=65536,t=2,p=1$YzI5dFpYTmhiSFFBQUFBQUFBQUFBQQ$Jxy74cswY2mq9y+u+iJcJy8EqOp4t/C7DWDzGwGB3IM", nil) - ).to be_falsey - end -end diff --git a/spec/minitest/rubycheck_spec.rb b/spec/minitest/rubycheck_spec.rb deleted file mode 100644 index c930a86..0000000 --- a/spec/minitest/rubycheck_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -# This was supposed to use Rubycheck, however the current version doesn't run -# These property tests identified the NULL hash bug -describe 'Argon2PropertyTest' do - let(:run_count) { (ENV['TEST_CHECKS'] || 100).to_i } - - it 'test success' do - hashlist = {} - - run_count.times do - word = Argon2::Engine.saltgen - hashlist[word] = Argon2::Password.create(word) - end - - hashlist.each do |word, hash| - expect( - Argon2::Password.verify_password(word, hash) - ).to be_truthy - end - end - - it 'test fail' do - hashlist = {} - - run_count.times do - word = Argon2::Engine.saltgen - hashlist[word] = Argon2::Password.create(word) - end - - hashlist.each do |word, hash| - wrongword = Argon2::Engine.saltgen - expect( - Argon2::Password.verify_password(wrongword, hash) - ).to be_falsey - end - end -end diff --git a/spec/minitest/saltlen_spec.rb b/spec/minitest/saltlen_spec.rb deleted file mode 100644 index 8a12f02..0000000 --- a/spec/minitest/saltlen_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe 'SaltlenTest' do - it 'long salt' do - expect( - Argon2::Password.verify_password("password", "$argon2i$v=19$m=65536,t=2,p=1$VG9vTG9uZ1NhbGVMZW5ndGg$mYleBHsG6N0+H4JGJ0xXoIRO6rWNZwN/eQQQ8eHIDmk") - ).to be_truthy - end - - it 'short salt' do - expect( - Argon2::Password.verify_password("password", "$argon2i$v=19$m=65536,t=2,p=1$VG9vU2hvcnRTYWxlTGVu$i59ELgAm5G6J+9+oZwO+kkV48tJyocNh6bHdkj9J5lk") - ).to be_truthy - end -end diff --git a/spec/minitest/unicode_spec.rb b/spec/minitest/unicode_spec.rb deleted file mode 100644 index 7566a43..0000000 --- a/spec/minitest/unicode_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe 'UnicodeTest' do - it 'accepts utf16' do - # A string found on Google which encodes with a NULL byte - unstr = "Σὲ γνωρίζω ἀπὸ τὴν κό".encode("utf-16le") - argon2 = Argon2::Password.create(unstr) - expect(Argon2::Password.verify_password(unstr, argon2)).to be_truthy - end - - it 'accepts null byte' do - rawstr = "String has a\0NULL in it" - argon2 = Argon2::Password.create(rawstr) - expect(Argon2::Password.verify_password(rawstr, argon2)).to be_truthy - # Asserts that no NULL byte truncation occurs - expect(Argon2::Password.verify_password("String has a", argon2)).to be_falsey - end - - it 'accepts emoji 😄' do - rawstr = "😀 😬 😁 😂 😃 😄 💩 😈 👿" - argon2 = Argon2::Password.create(rawstr) - expect(Argon2::Password.verify_password(rawstr, argon2)).to be_truthy - expect(Argon2::Password.verify_password("", argon2)).to be_falsey - expect(Argon2::Password.verify_password(" ", argon2)).to be_falsey - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index 08030d0..0000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '../lib')) -$LOAD_PATH.unshift(File.dirname(__FILE__)) - -require 'byebug' -require 'faker' - -############################ -## Generate Test Coverage ## -############################ - -unless ENV['coverage'] == 'false' - require 'simplecov' - require 'simplecov-lcov' - SimpleCov::Formatter::LcovFormatter.config do |c| - c.report_with_single_file = true - c.output_directory = 'coverage/lvoc' - c.lcov_file_name = 'lcov.info' - c.single_report_path = 'coverage/lcov.info' - end - SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter - SimpleCov.start -end - -##################### -## Configure RSpec ## -##################### - -RSpec.configure do |config| - # These two settings work together to allow you to limit a spec run - # to individual examples or groups you care about by tagging them with - # `:focus` metadata. When nothing is tagged with `:focus`, all examples - # get run. - config.filter_run :focus - config.run_all_when_everything_filtered = true - - # Find load order dependencies - config.order = :random - # Allow replicating load order dependency - # by passing in same seed using --seed - Kernel.srand config.seed - - # Find slow specs by running `profiling=true rspec` - config.profile_examples = 5 if ENV['profiling'] == 'true' -end - -###################### -## Load the Library ## -###################### - -require 'argon2' diff --git a/test/api_test.rb b/test/api_test.rb new file mode 100644 index 0000000..cd299a2 --- /dev/null +++ b/test/api_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'test_helper' + +module Argon2 + # Simple stub to facilitate testing these variables + class Password + attr_accessor :t_cost, :m_cost, :secret + end +end + +class Argon2APITest < Minitest::Test + def test_create_default + assert pass = Argon2::Password.new + assert_instance_of Argon2::Password, pass + assert_equal 16, pass.m_cost + assert_equal 2, pass.t_cost + assert_nil pass.secret + end + + def test_create_args + assert pass = Argon2::Password.new(t_cost: 4, m_cost: 12) + assert_instance_of Argon2::Password, pass + assert_equal 12, pass.m_cost + assert_equal 4, pass.t_cost + assert_nil pass.secret + end + + def test_secret + assert pass = Argon2::Password.new(secret: "A secret") + assert_equal pass.secret, "A secret" + end + + def test_hash + assert pass = Argon2::Password.new + assert pass.create('mypassword') + end + + def test_valid_hash + secure_pass = Argon2::Password.create('A secret') + assert Argon2::Password.valid_hash?(secure_pass) + end +end diff --git a/test/engine_test.rb b/test/engine_test.rb new file mode 100644 index 0000000..cc90c6b --- /dev/null +++ b/test/engine_test.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'test_helper' + +class EngineTest < Minitest::Test + def test_saltgen + generate = [] + assert(10.times { generate << Argon2::Engine.saltgen }) + duplicates = generate.select { |e| generate.count(e) > 1 } + assert_equal duplicates.length, 0 + wrong_length = generate.reject do |e| + e.length == Argon2::Constants::SALT_LEN + end + assert_equal wrong_length.size, 0 + end +end diff --git a/spec/minitest/error_spec.rb b/test/error_test.rb similarity index 57% rename from spec/minitest/error_spec.rb rename to test/error_test.rb index 3a4851f..be87dbb 100644 --- a/spec/minitest/error_spec.rb +++ b/test/error_test.rb @@ -1,31 +1,31 @@ # frozen_string_literal: true -require 'spec_helper' +require 'test_helper' -describe 'Argon2ErrorTest' do - it 'ffi fail' do - expect{ +class Argon2ErrorTest < Minitest::Test + def test_ffi_fail + assert_raises Argon2::ArgonHashFail do Argon2::Engine.hash_argon2i("password", "somesalt\0\0\0\0\0\0\0\0", 2, 1) - }.to raise_error Argon2::Error + end end - it 'memory too small' do - expect{ + def test_memory_too_small + assert_raises Argon2::ArgonHashFail do Argon2::Engine.hash_argon2id_encode("password", "somesalt\0\0\0\0\0\0\0\0", 2, 1, nil) - }.to raise_error Argon2::Error + end end - it 'salt size' do - expect{ + def test_salt_size + assert_raises Argon2::ArgonHashFail do Argon2::Engine.hash_argon2id_encode("password", "somesalt", 2, 16, nil) - }.to raise_error Argon2::Error + end end - it 'password null' do - expect{ + def test_passwd_null + assert_raises Argon2::ArgonHashFail do Argon2::Engine.hash_argon2id_encode(nil, "somesalt\0\0\0\0\0\0\0\0", 2, 16, nil) - }.to raise_error Argon2::Error + end end end diff --git a/test/key_test.rb b/test/key_test.rb new file mode 100644 index 0000000..2360997 --- /dev/null +++ b/test/key_test.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'test_helper' + +class LowLevelArgon2Test < Minitest::Test + KEY = "a magic key" + PASS = "random password" + def test_key_hash + # Default hash + argon = Argon2::Password.new(t_cost: 2, m_cost: 16) + assert basehash = argon.create(PASS) + # Keyed hash + argon = Argon2::Password.new(t_cost: 2, m_cost: 16, secret: KEY) + assert keyhash = argon.create(PASS) + refute_equal basehash, keyhash + # The keyed hash - without the key + refute Argon2::Password.verify_password(PASS, keyhash) + # With key + assert Argon2::Password.verify_password(PASS, keyhash, KEY) + end +end diff --git a/test/legacy.rb b/test/legacy.rb new file mode 100644 index 0000000..455a94c --- /dev/null +++ b/test/legacy.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'test_helper' +# frozen_string_literal: true + +class Legacy < Minitest::Test + HASH_1_0 = "$argon2i$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$+r0d29hqEB0yasKr55ZgICsQGSkl0v0kgwhd+U3wyRo" + HASH_1_1 = "$argon2i$v=19$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$+r0d29hqEB0yasKr55ZgICsQGSkl0v0kgwhd+U3wyRo" + HASH_0 = "$argon2i$v=16$m=256,t=2,p=1$c29tZXNhbHQ$/U3YPXYsSb3q9XxHvc0MLxur+GP960kN9j7emXX8zwY" + def test_legacy_hashes + # These are the hash formats for 1.0 and 1.1 of this gem. + assert Argon2::Password.verify_password(PASS, HASH_1_0) + assert Argon2::Password.verify_password(PASS, HASH_1_1) + assert Argon2::Password.verify_password(PASS, HASH_0) + end + + def test_valid_hash_legacy_hashes + # These are the hash formats for 1.0 and 1.1 of this gem. + assert Argon2::Password.valid_hash?(PASS, HASH_1_0) + assert Argon2::Password.valid_hash?(PASS, HASH_1_1) + assert Argon2::Password.valid_hash?(PASS, HASH_0) + end +end diff --git a/test/low_level_test.rb b/test/low_level_test.rb new file mode 100644 index 0000000..d62eead --- /dev/null +++ b/test/low_level_test.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'test_helper' + +# Vectors in this suite taken from test.c in reference + +class LowLevelArgon2Test < Minitest::Test + def test_that_it_has_a_version_number + refute_nil ::Argon2::VERSION + end + + def test_ffi_vector + assert_equal Argon2::Engine.hash_argon2i( + "password", "somesalt\0\0\0\0\0\0\0\0", 2, 16), + '1c7eeef9e0e969b3024722fc864a1ca9f6ca20da73f9bf3f1731881beae2039e' + + assert_equal Argon2::Engine.hash_argon2i( + "password", "somesalt\0\0\0\0\0\0\0\0", 2, 18), + '5c6dfd2712110cf88f1426059b01d87f8210d5368da0e7ee68586e9d4af4954b' + + assert_equal Argon2::Engine.hash_argon2i( + "password", "somesalt\0\0\0\0\0\0\0\0", 2, 8), + 'dfebf9d4eadd6859f4cc6a9bb20043fd9da7e1e36bdacdbb05ca569f463269f8' + + assert_equal Argon2::Engine.hash_argon2i( + "password", "somesalt\0\0\0\0\0\0\0\0", 1, 16), + 'fabd1ddbd86a101d326ac2abe79660202b10192925d2fd2483085df94df0c91a' + + assert_equal Argon2::Engine.hash_argon2i( + "password", "somesalt\0\0\0\0\0\0\0\0", 4, 16), + 'b3b4cb3d6e2c1cb1e7bffdb966ab3ceafae701d6b7789c3f1e6c6b22d82d99d5' + + assert_equal Argon2::Engine.hash_argon2i( + "differentpassword", "somesalt\0\0\0\0\0\0\0\0", 2, 16), + 'b2db9d7c0d1288951aec4b6e1cd3835ea29a7da2ac13e6f48554a26b127146f9' + + assert_equal Argon2::Engine.hash_argon2i( + "password", "diffsalt\0\0\0\0\0\0\0\0", 2, 16), + 'bb6686865f2c1093f70f543c9535f807d5b42d5dc6d71f14a4a7a291913e05e0' + + assert_equal Argon2::Engine.hash_argon2i( + "password", "somesaltsomesalt", 2, 16, 16), + '85d58a069b81f7606dc772810d00496d' + + assert_equal Argon2::Engine.hash_argon2id( + "password", "somesaltsomesalt", 2, 16), + 'fc33b78139231d34b71626bd6245c1d72efa190ad605c3d8166a72adcedfa2c2' + end + + def test_encoded_hash + assert_equal Argon2::Engine.hash_argon2id_encode( + "password", "somesalt\0\0\0\0\0\0\0\0", 2, 16, nil), + '$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQAAAAAAAAAAA$syf8TzB9pvMIGtFhvRATHW1nX43iP+FLaaTXnqpyMrY' + + assert_equal Argon2::Engine.hash_argon2id_encode( + "password", "somesalt\0\0\0\0\0\0\0\0", 2, 8, nil), + '$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQAAAAAAAAAAA$TCsNUutWgv3lowstIasFJbdiamKiq8qPUdz2wSvQ4rw' + + assert_equal Argon2::Engine.hash_argon2id_encode( + "password", "somesalt\0\0\0\0\0\0\0\0", 1, 16, nil), + '$argon2id$v=19$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$b7sLmBJ4YGj/yOjMnUDWC1dvrtZr7EPdMT6zB9Fq0pk' + + assert_equal Argon2::Engine.hash_argon2id_encode( + "differentpassword", "somesalt\0\0\0\0\0\0\0\0", 2, 16, nil), + '$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQAAAAAAAAAAA$0bDR2fpiZutijzxlxrjLnqnCSmtG1/reR4QNcavfKLk' + + assert_equal Argon2::Engine.hash_argon2id_encode( + "password", "diffsalt\0\0\0\0\0\0\0\0", 2, 16, nil), + '$argon2id$v=19$m=65536,t=2,p=1$ZGlmZnNhbHQAAAAAAAAAAA$vm1qQXZQ+/MgT2Y+Go7XnxtA9dJz3wotjfg0itOgKlY' + end + + def test_verify + assert Argon2::Engine.argon2_verify("password", "$argon2i$v=19$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$+r0d29hqEB0yasKr55ZgICsQGSkl0v0kgwhd+U3wyRo", nil) + refute Argon2::Engine.argon2_verify("notword", "$argon2i$v=19$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$+r0d29hqEB0yasKr55ZgICsQGSkl0v0kgwhd+U3wyRo", nil) + assert Argon2::Engine.argon2_verify("password", "$argon2id$v=19$m=262144,t=2,p=1$c29tZXNhbHQ$eP4eyR+zqlZX1y5xCFTkw9m5GYx0L5YWwvCFvtlbLow", nil) + refute Argon2::Engine.argon2_verify("password", "$argon2id$v=19$m=262144,t=2,p=1$c29tZXNhbHQ$eP4eyR+zqlZX1y5xCFTkw9m5GYx0L5YWwvCFvtlbLok", nil) + assert Argon2::Engine.argon2_verify("password", "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc", nil) + refute Argon2::Engine.argon2_verify("notword", "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ$CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc", nil) + assert Argon2::Engine.argon2_verify("password", "$argon2d$v=19$m=65536,t=2,p=1$YzI5dFpYTmhiSFFBQUFBQUFBQUFBQQ$Jxy74cswY2mq9y+u+iJcJy8EqOp4t/C7DWDzGwGB3IM", nil) + refute Argon2::Engine.argon2_verify("notword", "$argon2d$v=19$m=65536,t=2,p=1$YzI5dFpYTmhiSFFBQUFBQUFBQUFBQQ$Jxy74cswY2mq9y+u+iJcJy8EqOp4t/C7DWDzGwGB3IM", nil) + end +end diff --git a/test/rubycheck_test.rb b/test/rubycheck_test.rb new file mode 100644 index 0000000..3b351c3 --- /dev/null +++ b/test/rubycheck_test.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'test_helper' + +TIMES = (ENV['TEST_CHECKS'] || 100).to_i + +# This was supposed to use Rubycheck, however the current version doesn't run +# These property tests identified the NULL hash bug +class Argon2PropertyTest < Minitest::Test + def test_success + hashlist = {} + TIMES.times do + word = Argon2::Engine.saltgen + assert hashlist[word] = Argon2::Password.create(word), + word.unpack('H*').join + end + hashlist.each do |word, hash| + assert Argon2::Password.verify_password(word, hash), + word.unpack('H*').join + end + end + + def test_fail + hashlist = {} + TIMES.times do + word = Argon2::Engine.saltgen + assert hashlist[word] = Argon2::Password.create(word), + word.unpack('H*').join + end + hashlist.each do |word, hash| + wrongword = Argon2::Engine.saltgen + refute Argon2::Password.verify_password(wrongword, hash), + word.unpack('H*').join + end + end +end diff --git a/test/saltlen_test.rb b/test/saltlen_test.rb new file mode 100644 index 0000000..397bc92 --- /dev/null +++ b/test/saltlen_test.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'test_helper' + +class SaltlenTest < Minitest::Test + # Tests relevant to issue #14 + def test_longsalt + # A test for verifying existing hashes with diffferent salt lengths + assert Argon2::Password.verify_password("password", + "$argon2i$v=19$m=65536,t=2,p=1$VG9vTG9uZ1NhbGVMZW5ndGg$mYleBHsG6N0+H4JGJ0xXoIRO6rWNZwN/eQQQ8eHIDmk") + end + + def test_shortsalt + # Asserts that no NULL byte truncation occurs + assert Argon2::Password.verify_password("password", + "$argon2i$v=19$m=65536,t=2,p=1$VG9vU2hvcnRTYWxlTGVu$i59ELgAm5G6J+9+oZwO+kkV48tJyocNh6bHdkj9J5lk") + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..c5490da --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift File.expand_path('../lib', __dir__) + +require 'simplecov' +require 'simplecov-lcov' +SimpleCov::Formatter::LcovFormatter.config do |c| + c.report_with_single_file = true + c.output_directory = 'coverage/lvoc' + c.lcov_file_name = 'lcov.info' + c.single_report_path = 'coverage/lcov.info' +end +SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter +SimpleCov.start + +require 'argon2' + +require 'minitest/autorun' diff --git a/test/unicode_test.rb b/test/unicode_test.rb new file mode 100644 index 0000000..d406280 --- /dev/null +++ b/test/unicode_test.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'test_helper' + +class UnicodeTest < Minitest::Test + def test_utf16 + # A string found on Google which encodes with a NULL byte + unstr = "Σὲ γνωρίζω ἀπὸ τὴν κό".encode("utf-16le") + assert hash = Argon2::Password.create(unstr) + assert Argon2::Password.verify_password(unstr, hash) + end + + def test_null_byte + rawstr = "String has a\0NULL in it" + hash = Argon2::Password.create(rawstr) + assert Argon2::Password.verify_password(rawstr, hash) + # Asserts that no NULL byte truncation occurs + refute Argon2::Password.verify_password("String has a", hash), + "Does not NULL truncate" + end + + def test_emoji + rawstr = "😀 😬 😁 😂 😃 😄 💩 😈 👿" + hash = Argon2::Password.create(rawstr) + assert Argon2::Password.verify_password(rawstr, hash) + refute Argon2::Password.verify_password("", hash) + end +end From ff9f2c32f404a62326183eea8af41733f105df93 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sun, 28 Mar 2021 07:49:48 -0700 Subject: [PATCH 12/35] Refactor minitest suite to support new changes --- test/api_test.rb | 23 ++++++++++++----------- test/error_test.rb | 8 ++++---- test/key_test.rb | 6 ++---- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/test/api_test.rb b/test/api_test.rb index cd299a2..6d52112 100644 --- a/test/api_test.rb +++ b/test/api_test.rb @@ -3,37 +3,38 @@ require 'test_helper' module Argon2 - # Simple stub to facilitate testing these variables - class Password - attr_accessor :t_cost, :m_cost, :secret - end + # Simple stub to facilitate testing these variables (stub now unnecessary) + # class Password + # attr_accessor :t_cost, :m_cost, :secret + # end end class Argon2APITest < Minitest::Test def test_create_default - assert pass = Argon2::Password.new + assert pass = Argon2::Password.create('mypassword') assert_instance_of Argon2::Password, pass assert_equal 16, pass.m_cost assert_equal 2, pass.t_cost - assert_nil pass.secret + # assert_nil pass.secret # Secret is not persisted on the Argon2::Password end def test_create_args - assert pass = Argon2::Password.new(t_cost: 4, m_cost: 12) + assert pass = Argon2::Password.create('mypassword', t_cost: 4, m_cost: 12) assert_instance_of Argon2::Password, pass assert_equal 12, pass.m_cost assert_equal 4, pass.t_cost - assert_nil pass.secret + # assert_nil pass.secret # Secret is not persisted on the Argon2::Password end + # For usage of the secret param, see key_test.rb def test_secret - assert pass = Argon2::Password.new(secret: "A secret") + assert pass = Argon2::Password.create('mypassword', secret: "A secret") + skip("The secret isn't kept on the Argon2::Password instance") assert_equal pass.secret, "A secret" end def test_hash - assert pass = Argon2::Password.new - assert pass.create('mypassword') + assert pass = Argon2::Password.create('mypassword') end def test_valid_hash diff --git a/test/error_test.rb b/test/error_test.rb index be87dbb..3f8dbe6 100644 --- a/test/error_test.rb +++ b/test/error_test.rb @@ -4,26 +4,26 @@ class Argon2ErrorTest < Minitest::Test def test_ffi_fail - assert_raises Argon2::ArgonHashFail do + assert_raises Argon2::Error do Argon2::Engine.hash_argon2i("password", "somesalt\0\0\0\0\0\0\0\0", 2, 1) end end def test_memory_too_small - assert_raises Argon2::ArgonHashFail do + assert_raises Argon2::Error do Argon2::Engine.hash_argon2id_encode("password", "somesalt\0\0\0\0\0\0\0\0", 2, 1, nil) end end def test_salt_size - assert_raises Argon2::ArgonHashFail do + assert_raises Argon2::Error do Argon2::Engine.hash_argon2id_encode("password", "somesalt", 2, 16, nil) end end def test_passwd_null - assert_raises Argon2::ArgonHashFail do + assert_raises Argon2::Error do Argon2::Engine.hash_argon2id_encode(nil, "somesalt\0\0\0\0\0\0\0\0", 2, 16, nil) end diff --git a/test/key_test.rb b/test/key_test.rb index 2360997..3c4b2f4 100644 --- a/test/key_test.rb +++ b/test/key_test.rb @@ -7,11 +7,9 @@ class LowLevelArgon2Test < Minitest::Test PASS = "random password" def test_key_hash # Default hash - argon = Argon2::Password.new(t_cost: 2, m_cost: 16) - assert basehash = argon.create(PASS) + assert basehash = Argon2::Password.create(PASS, t_cost: 2, m_cost: 16) # Keyed hash - argon = Argon2::Password.new(t_cost: 2, m_cost: 16, secret: KEY) - assert keyhash = argon.create(PASS) + assert keyhash = Argon2::Password.create(PASS, t_cost: 2, m_cost: 16, secret: KEY) refute_equal basehash, keyhash # The keyed hash - without the key refute Argon2::Password.verify_password(PASS, keyhash) From db7e8bcfc938695134a9413cbfd00a854ea41770 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sun, 28 Mar 2021 07:53:47 -0700 Subject: [PATCH 13/35] Revert Gemfile and gemspec changes, nice-to-haves --- Gemfile | 18 +----------------- argon2.gemspec | 7 ++++++- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/Gemfile b/Gemfile index d491e9f..ea62c3a 100644 --- a/Gemfile +++ b/Gemfile @@ -2,21 +2,5 @@ source 'https://rubygems.org' +# Specify your gem's dependencies in argon2.gemspec gemspec - -# TODO: Remove/relax version locks where possible -group :test do - gem 'bundler', '~> 2.0' - gem 'minitest', '~> 5.8' - gem 'rake', '~> 13.0.1' - gem 'rubocop', '~> 1.7' - gem 'simplecov', '~> 0.20' - gem 'simplecov-lcov', '~> 0.8' - - # gem 'byebug' - # gem 'faker' - # gem 'rspec' - # gem 'shoulda-matchers' - # gem 'simplecov', require: false - # gem 'timecop' # TODO: Unused, remove? -end diff --git a/argon2.gemspec b/argon2.gemspec index 3427343..d3eb332 100644 --- a/argon2.gemspec +++ b/argon2.gemspec @@ -29,9 +29,14 @@ Gem::Specification.new do |spec| spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.add_dependency 'ffi', '~> 1.14' spec.add_dependency 'ffi-compiler', '~> 1.0' + spec.add_development_dependency "bundler", '~> 2.0' + spec.add_development_dependency "minitest", '~> 5.8' + spec.add_development_dependency "rake", '~> 13.0.1' + spec.add_development_dependency "rubocop", '~> 1.7' + spec.add_development_dependency "simplecov", '~> 0.20' + spec.add_development_dependency "simplecov-lcov", '~> 0.8' spec.extensions << 'ext/argon2_wrap/extconf.rb' end From 94ad58503c324e1bd1fb00597f84e433de5d018b Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sun, 28 Mar 2021 08:12:59 -0700 Subject: [PATCH 14/35] Minimize styling changes --- .github/workflows/rubocop.yml | 1 + .github/workflows/ruby.yml | 2 +- .gitignore | 3 - README.md | 177 +++++++++++----------------------- 4 files changed, 58 insertions(+), 125 deletions(-) diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml index 8b8de92..3deeaa7 100644 --- a/.github/workflows/rubocop.yml +++ b/.github/workflows/rubocop.yml @@ -13,3 +13,4 @@ jobs: uses: gimenete/rubocop-action@1.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 69435a4..9a531a9 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Ruby ${{ matrix.ruby-version }} uses: ruby/setup-ruby@v1 with: - ruby-version: ${{ matrix.ruby-version }} + ruby-version: ${{ matrix.ruby-version }} - name: Install dependencies run: bundle install - name: Init submodules diff --git a/.gitignore b/.gitignore index 4464ba1..d2a1432 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,3 @@ /ext/argon2_wrap/tests /ext/argon2_wrap/libargon2_wrap.bundle* argon2-0.0.2.gem - -# Ignore Byebug command history file. -.byebug_history diff --git a/README.md b/README.md index 26befce..0ad2ff9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ # Ruby Argon2 Gem -This Ruby Gem provides FFI bindings, and a simplified interface, to the Argon2 -algorithm. [Argon2](https://github.com/P-H-C/phc-winner-argon2) is the official -winner of the Password Hashing Competition, a several year project to identify a -successor to bcrypt/PBKDF/scrypt methods of securely storing passwords. This is -an independent project and not official from the PHC team. +This Ruby Gem provides FFI bindings, and a simplified interface, to the Argon2 algorithm. [Argon2](https://github.com/P-H-C/phc-winner-argon2) is the official winner of the Password Hashing Competition, a several year project to identify a successor to bcrypt/PBKDF/scrypt methods of securely storing passwords. This is an independant project and not official from the PHC team. ![Build Status](https://github.com/technion/ruby-argon2/workflows/Test%20Suite/badge.svg) [![Code Climate](https://codeclimate.com/github/technion/ruby-argon2/badges/gpa.svg)](https://codeclimate.com/github/technion/ruby-argon2) @@ -14,29 +10,15 @@ an independent project and not official from the PHC team. This project has several key tenets to its design: -* The reference Argon2 implementation is to be used "unaltered". To ensure - compliance with this goal, and encourage regular updates from upstream, the - upstream library is implemented as a git submodule, and is intended to stay - that way. -* The FFI interface is kept as slim as possible, with wrapper classes preferred - to implementing context structs in FFI -* Security and maintainability take top priority. This can have an impact on - platform support. A PR that contains platform specific code paths is unlikely - to be accepted. -* Tested platforms are MRI Ruby 2.2, 2.3 and JRuby 9000. No assertions are made - on other platforms. -* Errors from the C interface are raised as Exceptions. There are a lot of - exception classes, but they tend to relate to things like very broken input, - and code bugs. Calls to this library should generally not require a rescue. +* The reference Argon2 implementation is to be used "unaltered". To ensure compliance with this goal, and encourage regular updates from upstream, the upstream library is implemented as a git submodule, and is intended to stay that way. +* The FFI interface is kept as slim as possible, with wrapper classes preferred to implementing context structs in FFI +* Security and maintainability take top priority. This can have an impact on platform support. A PR that contains platform specific code paths is unlikely to be accepted. +* Tested platforms are MRI Ruby 2.2, 2.3 and JRuby 9000. No assertions are made on other platforms. +* Errors from the C interface are raised as Exceptions. There are a lot of exception classes, but they tend to relate to things like very broken input, and code bugs. Calls to this library should generally not require a rescue. * Test suites should aim for 100% code coverage. -* Default work values should not be considered constants. I will increase them - from time to time. -* Not exposing the threads parameter is a design choice. I believe there is - significant risk, and minimal gain in using a value other than '1'. Four - threads on a four core box completely ties up the entire server to process one - user logon. If you want more security, increase m_cost. -* Many Rubocop errors have been disabled, but any commit should avoid new alerts - or demonstrate their necessity. +* Default work values should not be considered constants. I will increase them from time to time. +* Not exposing the threads parameter is a design choice. I believe there is significant risk, and minimal gain in using a value other than '1'. Four threads on a four core box completely ties up the entire server to process one user logon. If you want more security, increase m_cost. +* Many Rubocop errors have been disabled, but any commit should avoid new alerts or demonstrate their necessity. ## Usage @@ -61,11 +43,7 @@ hasher = Argon2::Password.new hasher.create("password") ``` -If you follow this pattern, it is important to create a new `Argon2::Password` -every time you generate a hash, in order to ensure a unique salt. See -[issue 23](https://github.com/technion/ruby-argon2/issues/23) for more -information. - +If you follow this pattern, it is important to create a new `Argon2::Password` every time you generate a hash, in order to ensure a unique salt. See [issue 23](https://github.com/technion/ruby-argon2/issues/23) for more information. Alternatively, use this shortcut: ```ruby @@ -73,8 +51,7 @@ Argon2::Password.create("password") => "$argon2i$v=19$m=65536,t=2,p=1$61qkSyYNbUgf3kZH3GtHRw$4CQff9AZ0lWd7uF24RKMzqEiGpzhte1Hp8SO7X8bAew" ``` -You can then use this function to verify a password against a given hash. Will -return either true or false. +You can then use this function to verify a password against a given hash. Will return either true or false. ```ruby Argon2::Password.verify_password("password", secure_password) @@ -87,9 +64,7 @@ Argon2::Password.verify_password("password", "$argon2id$v=19$m=262144,t=2,p=1$c2 => true ``` -Argon2 supports an optional key value. This should be stored securely on your -server, such as alongside your database credentials. Hashes generated with a key -will only validate when presented that key. +Argon2 supports an optional key value. This should be stored securely on your server, such as alongside your database credentials. Hashes generated with a key will only validate when presented that key. ```ruby KEY = "A key" @@ -98,136 +73,97 @@ myhash = argon.create("A password") Argon2::Password.verify_password("A password", myhash, KEY) ``` -## Migrating to v3.0 +## Version 3.0 - Breaking API changes, addition of new helper functions -### Errors restructured +### Argon2::Password API refactored -**The root level error for Argon2 has been renamed.** +**Argon2::Password.new and Argon2::Password.create are now different.** -Find and replace all instances of: +Argon2::Passwords can now be created without initializing an instance first. ```ruby -Argon2::ArgonHashFail +# Take instances where we abstract creating the password by first exposing an Object instance +instance = Argon2::Password.new(m_cost: some_m_cost) +instance.create(input_password) + +# And remove the abstraction step +Argon2::Password.create(input_password, m_cost: some_m_cost) ``` -With: +### Errors restructured -```ruby -Argon2::Error -``` +**The root level error for Argon2 has been renamed.** -Additionally, the following new Errors have been added for capturing specific -exceptions. You aren't required to use them; they are provided purely as a way -to catch more specific exceptions as needed. +Argon2::ArgonHashFail has been renamed to Argon2::Error ```ruby -Argon2::Errors::InvalidHash -Argon2::Errors::InvalidVersion # Inherits from Argon2::Errors::InvalidHash -Argon2::Errors::InvalidCost # Inherits from Argon2::Errors::InvalidHash -Argon2::Errors::InvalidTCost # Inherits from Argon2::Errors::InvalidCost -Argon2::Errors::InvalidMCost # Inherits from Argon2::Errors::InvalidCost -Argon2::Errors::InvalidPCost # Inherits from Argon2::Errors::InvalidCost -Argon2::Errors::InvalidPassword -Argon2::Errors::InvalidSaltSize -Argon2::Errors::InvalidOutputLength -Argon2::Errors::ExtError +# Find any instances of Argon2::ArgonHashFail, for example... +def login(username, password) + [...] +rescue Argon2::ArgonHashFail + [...] +end + +# And do a straight 1:1 replacement +def login(username, password) + [...] +rescue Argon2::Error + [...] +end ``` -### Argon2::Engine and Argon2::Password refactored - -TODO: Write this guide once the helpers are agreed upon and finalized. - ## Version 2.0 - Argon 2id - -Version 2.x upwards will now default to the Argon2id hash format. This is -consistent with current recommendations regarding Argon2 usage. It remains -capable of verifying existing hashes. +Version 2.x upwards will now default to the Argon2id hash format. This is consistent with current recommendations regarding Argon2 usage. It remains capable of verifying existing hashes. ## Important notes regarding version 1.0 upgrade +Version 1.0.0 included a major version bump over 0.1.4 due to several breaking changes. The first of these was an API change, which you can read the background on [here](https://github.com/technion/ruby-argon2/issues/9). -Version 1.0.0 included a major version bump over 0.1.4 due to several breaking -changes. The first of these was an API change, which you can read the background -on [here](https://github.com/technion/ruby-argon2/issues/9). - -The second of these is that the reference Argon2 implementation introduced an -algorithm change, which produces a hash which is not backwards compatible. This -is documented on -[this PR on the C library](https://github.com/P-H-C/phc-winner-argon2/pull/115). -This was a regrettable requirement to address a security concern in the -algorithm itself. The two versions of the Argon2 algorithm are numbered 1.0 and -1.3 respectively. +The second of these is that the reference Argon2 implementation introduced an algorithm change, which produces a hash which is not backwards compatible. This is documented on [this PR on the C library](https://github.com/P-H-C/phc-winner-argon2/pull/115). This was a regrettable requirement to address a security concern in the algorithm itself. The two versions of the Argon2 algorithm are numbered 1.0 and 1.3 respectively. -Shortly after this, version 1.0.0 of this gem was released with this breaking -change, supporting only Argon2 v1.3. Further time later, the official encoding -format was updated, with a spec that included the version number, and the -library introduced backward compatibility. This should remove the likelihood of -such breaking changes in future. Version 1.1.0 will silently introduce the -current version number in hashes, in order to avoid a further compatibility -break. +Shortly after this, version 1.0.0 of this gem was released with this breaking change, supporting only Argon2 v1.3. Further time later, the official encoding format was updated, with a spec that included the version number, and the library introduced backward compatibility. This should remove the likelihood of such breaking changes in future. Version 1.1.0 will silently introduce the current version number in hashes, in order to avoid a further compatibility break. ## Platform Issues -The default installation workflow has caused issues with a number of gems under -the latest OSX. There is some excellent documentation on the issue and some -workarounds in the -[Jekyll Documentation](http://jekyllrb.com/docs/troubleshooting/#jekyll-amp-mac-os-x-1011). -With this in mind, OSX is a fully supported OS. +The default installation workflow has caused issues with a number of gems under the latest OSX. There is some excellent documentation on the issue and some workarounds in the [Jekyll Documentation](http://jekyllrb.com/docs/troubleshooting/#jekyll-amp-mac-os-x-1011). With this in mind, OSX is a fully supported OS. -Windows is not. Nobody anywhere has the resources to support Ruby FFI code on -Windows. +Windows is not. Nobody anywhere has the resources to support Ruby FFI code on Windows. -grsec introduces certain challenges. Please see -[documentation here](https://github.com/technion/ruby-argon2/issues/15). +grsec introduces certain challenges. Please see [documentation here](https://github.com/technion/ruby-argon2/issues/15). See the .travis.yml file to see currently tested and supported Ruby versions. ## RubyDocs documentation -[The usual URL](http://www.rubydoc.info/gems/argon2) will provide detailed -documentation. +[The usual URL](http://www.rubydoc.info/gems/argon2) will provide detailed documentation. ## FAQ ### Don't roll your own crypto! -This gets its own section because someone will raise it. I did not invent or -alter this algorithm, or implement it directly. These bindings were written -following -[considerable involvement with the C reference](https://github.com/P-H-C/phc-winner-argon2/commits/master?author=technion), -and a strong focus has been made on following the intent of the algorithm. +This gets its own section because someone will raise it. I did not invent or alter this algorithm, or implement it directly. These bindings were written following [considerable involvement with the C reference](https://github.com/P-H-C/phc-winner-argon2/commits/master?author=technion), and a strong focus has been made on following the intent of the algorithm. -It is strongly advised to avoid implementations that utilise off-spec methods of -introducing salts, invent imaginary parameters, or which use the word -"encryption" in describing the password hashing process +It is strongly advised to avoid implementations that utilise off-spec methods of introducing salts, invent imaginary parameters, or which use the word "encryption" in describing the password hashing process ### Secure wipe is useless -Although the low level C contains support for "secure memory wipe", any code -hitting that layer has copied your password to a dozen places in memory. It -should be assumed that such functionality does not exist. +Although the low level C contains support for "secure memory wipe", any code hitting that layer has copied your password to a dozen places in memory. It should be assumed that such functionality does not exist. ### Work maximums may be tighter than reference -The reference implementation is aimed to provide secure hashing for many years. -This implementation doesn't want you to DoS yourself in the meantime. -Accordingly, some artificial limits exist on work powers. This gem can be much -more agile in raising these as technology progresses. +The reference implementation is aimed to provide secure hashing for many years. This implementation doesn't want you to DoS yourself in the meantime. Accordingly, some artificial limits exist on work powers. This gem can be much more agile in raising these as technology progresses. ### Salts in general -If you are providing your own salt, you are probably using it wrong. The design -of any secure hashing system should take care of it for you. +If you are providing your own salt, you are probably using it wrong. The design of any secure hashing system should take care of it for you. ## Contributing -Any form of contribution is appreciated, however, please review -[CONTRIBUTING.md](CONTRIBUTING.md). +Any form of contribution is appreciated, however, please review [CONTRIBUTING.md](CONTRIBUTING.md). ## Building locally/Tests -To build the gem locally, you will need to checkout the submodule and build it -manually: +To build the gem locally, you will need to checkout the submodule and build it manually: ```shell git submodule update --init --recursive @@ -237,8 +173,7 @@ make cd ../.. ``` -The test harness includes a property based test. To more strenuously perform -this test, you can tune the iterations parameter: +The test harness includes a property based test. To more strenuously perform this test, you can tune the iterations parameter: ```shell TEST_CHECKS=10000 bundle exec rake test @@ -246,5 +181,5 @@ TEST_CHECKS=10000 bundle exec rake test ## License -The gem is available as open source under the terms of the -[MIT License](http://opensource.org/licenses/MIT). +The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). + From a498dbd750c9e7e65a4e81b78077884f74ad4ab1 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sun, 28 Mar 2021 08:38:49 -0700 Subject: [PATCH 15/35] Remove TODOs and update Argon2::Password documentation --- lib/argon2/engine.rb | 3 --- lib/argon2/password.rb | 40 +++++++++++++++++++--------------------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/lib/argon2/engine.rb b/lib/argon2/engine.rb index bacea50..284e894 100644 --- a/lib/argon2/engine.rb +++ b/lib/argon2/engine.rb @@ -3,9 +3,6 @@ require 'securerandom' module Argon2 - ## - # TODO: Document Engine class, and how it differs from the ffi_engine class. - # class Engine ## # Generates a random, binary string for use as a salt. diff --git a/lib/argon2/password.rb b/lib/argon2/password.rb index df52bfc..1c4f2d7 100644 --- a/lib/argon2/password.rb +++ b/lib/argon2/password.rb @@ -2,36 +2,20 @@ module Argon2 ## # Front-end API for the Argon2 module. # - # TODO: Find a good place to document the following knowledge, it's a PITA to - # google atm. - # - # 1) Variant used - # 2) Version used - # 3) Config used - # a) m - m_cost (memory) - # b) t - t_cost (time/stretches) - # c) p - p_cost (parallelism/cores) - # 4) Salt - # 5) Checksum - # - # | 1 | 2 | 3 | 4 | 5 (truncated) - # $argon2i$v=19$m=65536,t=2,p=1$VG9vTG9uZ1NhbGVMZW5ndGg$mYleBHsG6N0+H4JGJ0xXoI - # class Password - # + # Time Cost defaults DEFAULT_T_COST = 2 - DEFAULT_M_COST = 16 - # MIN_T_COST = 1 MAX_T_COST = 750 - # + # Memory Cost Defaults + DEFAULT_M_COST = 16 MIN_M_COST = 1 MAX_M_COST = 31 # The complete Argon2 digest string (not to be confused with the checksum). attr_reader :digest # The hash portion of the stored password hash. attr_reader :checksum - # The salt of the store password hash (including version and cost). + # The salt of the store password hash. attr_reader :salt # Variant used (argon2[i|d|id]) attr_reader :variant @@ -44,6 +28,9 @@ class Password # The parallelism cost factor used to create the hash. attr_reader :p_cost + ## + # Class methods + # class << self def create(password, options = {}) raise Argon2::Errors::InvalidPassword unless password.is_a?(String) @@ -71,12 +58,19 @@ def create(password, options = {}) ) end + ## # Supports 1 and argon2id formats. + # def valid_hash?(digest) /^\$argon2(id?|d).{,113}/ =~ digest end - # Provided for those who prefer the previous method of password comparison + ## + # Usage: + # + # Argon2::Password.verify_password(password, argon2_hash) + # Argon2::Password.verify_password(password, argon2_hash, secret) + # def verify_password(password, digest, secret = nil) digest = digest.to_s if password.is_a?(Argon2::Password) @@ -87,6 +81,10 @@ def verify_password(password, digest, secret = nil) end end + ###################### + ## Instance Methods ## + ###################### + ## # Initialize an Argon2::Password instance using any valid Argon2 digest. # From d14bb3ed192a47cb6ea1761850a6e8012a890846 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sun, 28 Mar 2021 08:39:26 -0700 Subject: [PATCH 16/35] Fix typo --- lib/argon2/password.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/argon2/password.rb b/lib/argon2/password.rb index 1c4f2d7..4db70d9 100644 --- a/lib/argon2/password.rb +++ b/lib/argon2/password.rb @@ -15,7 +15,7 @@ class Password attr_reader :digest # The hash portion of the stored password hash. attr_reader :checksum - # The salt of the store password hash. + # The salt of the stored password hash. attr_reader :salt # Variant used (argon2[i|d|id]) attr_reader :variant From 36c1d921a364b51f02e6d925b1cf767052fae7da Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sun, 28 Mar 2021 08:47:23 -0700 Subject: [PATCH 17/35] Fix slight misnomer in comments --- lib/argon2/password.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/argon2/password.rb b/lib/argon2/password.rb index 4db70d9..4c28ad4 100644 --- a/lib/argon2/password.rb +++ b/lib/argon2/password.rb @@ -3,11 +3,11 @@ module Argon2 # Front-end API for the Argon2 module. # class Password - # Time Cost defaults + # Time Cost constants DEFAULT_T_COST = 2 MIN_T_COST = 1 MAX_T_COST = 750 - # Memory Cost Defaults + # Memory Cost constants DEFAULT_M_COST = 16 MIN_M_COST = 1 MAX_M_COST = 31 From 54edb177b211ac4818a0acd12beb1f5e61f87fe2 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sun, 28 Mar 2021 09:16:13 -0700 Subject: [PATCH 18/35] Flesh out YARD documentation --- lib/argon2/constants.rb | 3 +-- lib/argon2/password.rb | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/lib/argon2/constants.rb b/lib/argon2/constants.rb index 3113546..a80b94c 100644 --- a/lib/argon2/constants.rb +++ b/lib/argon2/constants.rb @@ -3,10 +3,9 @@ module Argon2 ## # Constants utilised in several parts of the Argon2 module - # SALT_LEN is a standard recommendation from the Argon2 spec. # module Constants - SALT_LEN = 16 + SALT_LEN = 16 # Standard recommendation from the Argon2 spec OUT_LEN = 32 # Binary, unencoded output ENCODE_LEN = 108 # Encoded output end diff --git a/lib/argon2/password.rb b/lib/argon2/password.rb index 4c28ad4..e4e7072 100644 --- a/lib/argon2/password.rb +++ b/lib/argon2/password.rb @@ -3,13 +3,19 @@ module Argon2 # Front-end API for the Argon2 module. # class Password - # Time Cost constants + # Used as the default time cost if one isn't provided when calling + # Argon2::Password.create DEFAULT_T_COST = 2 + # Used to validate the minimum acceptable time cost MIN_T_COST = 1 + # Used to validate the maximum acceptable time cost MAX_T_COST = 750 - # Memory Cost constants + # Used as the default memory cost if one isn't provided when calling + # Argon2::Password.create DEFAULT_M_COST = 16 + # Used to validate the minimum acceptable memory cost MIN_M_COST = 1 + # Used to validate the maximum acceptable memory cost MAX_M_COST = 31 # The complete Argon2 digest string (not to be confused with the checksum). attr_reader :digest @@ -32,6 +38,23 @@ class Password # Class methods # class << self + ## + # Takes a user provided password and returns an Argon2::Password instance + # with the resulting Argon2 hash. + # + # Usage: + # + # Argon2::Password.create(password) + # Argon2::Password.create(password, t_cost: 4, m_cost: 20) + # Argon2::Password.create(password, secret: pepper) + # Argon2::Password.create(password, m_cost: 17, secret: pepper) + # + # Currently available options: + # + # * :t_cost + # * :m_cost + # * :secret + # def create(password, options = {}) raise Argon2::Errors::InvalidPassword unless password.is_a?(String) @@ -59,6 +82,8 @@ def create(password, options = {}) end ## + # Regex to validate if the provided String is a valid Argon2 hash output. + # # Supports 1 and argon2id formats. # def valid_hash?(digest) @@ -66,6 +91,13 @@ def valid_hash?(digest) end ## + # Takes a password, Argon2 hash, and optionally a secret, then uses the + # Argon2 C Library to verify if they match. + # + # Also accepts passing another Argon2::Password instance as the password, + # in which case it will compare the final Argon2 hash for each against + # each other. + # # Usage: # # Argon2::Password.verify_password(password, argon2_hash) From c0e149ada2b8fa6889b269fa5a0b13a3d2edde10 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sun, 28 Mar 2021 09:18:57 -0700 Subject: [PATCH 19/35] Fix yard thinking the argon variants were a link --- lib/argon2/password.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/argon2/password.rb b/lib/argon2/password.rb index e4e7072..63360c6 100644 --- a/lib/argon2/password.rb +++ b/lib/argon2/password.rb @@ -23,7 +23,7 @@ class Password attr_reader :checksum # The salt of the stored password hash. attr_reader :salt - # Variant used (argon2[i|d|id]) + # Variant used (argon2i / argon2d / argon2id) attr_reader :variant # The version of the argon2 algorithm used to create the hash. attr_reader :version From 3206644f478ff5d964edb30718acdc5cc2901af7 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sun, 28 Mar 2021 09:37:06 -0700 Subject: [PATCH 20/35] Reach 100% yard documentation, add error docs --- lib/argon2/errors.rb | 44 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/lib/argon2/errors.rb b/lib/argon2/errors.rb index 0864421..628d394 100644 --- a/lib/argon2/errors.rb +++ b/lib/argon2/errors.rb @@ -9,35 +9,71 @@ class Error < StandardError; end ## # Various errors for invalid parameters passed to the library. # - # WIP - # module Errors + ## + # Raised when an invalid Argon2 hash has been passed to Argon2::Password.new + # class InvalidHash < Argon2::Error; end + ## + # Raised when a valid Argon2 hash was passed to Argon2::Password, but the + # version information is missing or corrupted. + # class InvalidVersion < InvalidHash; end ## - # Not used directly, but allows developers to catch any cost exception - # regardless of which cost is invalid. + # Abstract error class that isn't raised directly, but allows you to catch + # any cost error, regardless of which value was invalid. # class InvalidCost < InvalidHash; end + ## + # Raised when an invalid time cost has been passed to + # Argon2::Password.create, or the hash passed to Argon2::Password.new + # was valid but the time cost information is missing or corrupted. + # class InvalidTCost < InvalidCost; end + ## + # Raised when an invalid memory cost has been passed to + # Argon2::Password.create, or the hash passed to Argon2::Password.new + # was valid but the memory cost information is missing or corrupted. + # class InvalidMCost < InvalidCost; end + ## + # Raised when an invalid parallelism cost has been passed to + # Argon2::Password.create, or the hash passed to Argon2::Password.new + # was valid but the parallelism cost information is missing or corrupted. + # class InvalidPCost < InvalidCost; end + ## + # Raised when a non-string object is passed to Argon2::Password.create + # class InvalidPassword < Argon2::Error def initialize(msg="Invalid password (expected a String)") super end end + ## + # Raised when an invalid salt length was passed to + # Argon2::Engine.hash_argon2id_encode + # class InvalidSaltSize < Argon2::Error; end + ## + # Raised when the output length passed to Argon2::Engine.hash_argon2i or + # Argon2::Engine.hash_argon2id is invalid. + # class InvalidOutputLength < Argon2::Error; end + ## + # Error raised by/caught from the Argon2 C Library. + # + # See Argon2::ERRORS for a full list of related error codes. + # class ExtError < Argon2::Error; end end From 5715a52e6932ee3f35225c6956b587b1e6d6117c Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Sun, 28 Mar 2021 19:58:06 -0700 Subject: [PATCH 21/35] Update type signatures for new password API (WIP) --- lib/argon2/password.rb | 3 +++ sig/argon2.rbs | 15 --------------- sig/engine.rbs | 6 ++++++ sig/ffi_engine.rbs | 9 +++++++++ sig/password.rbs | 40 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 58 insertions(+), 15 deletions(-) delete mode 100644 sig/argon2.rbs create mode 100644 sig/engine.rbs create mode 100644 sig/ffi_engine.rbs create mode 100644 sig/password.rbs diff --git a/lib/argon2/password.rb b/lib/argon2/password.rb index 63360c6..02370c9 100644 --- a/lib/argon2/password.rb +++ b/lib/argon2/password.rb @@ -134,6 +134,9 @@ def initialize(digest) @p_cost = split_digest[:p_cost] @salt = split_digest[:salt] @checksum = split_digest[:checksum] + # The return type is ignored by Object.new, this is provided purely for + # return type safety (rbs). + self else raise Argon2::Errors::InvalidHash end diff --git a/sig/argon2.rbs b/sig/argon2.rbs deleted file mode 100644 index 8da18d9..0000000 --- a/sig/argon2.rbs +++ /dev/null @@ -1,15 +0,0 @@ -# Classes -module Argon2 - class Password - @t_cost: Integer - @m_cost: Integer - @salt: nil | String - @secret: nil | String - - def initialize: (?Hash[Symbol, Integer] options) -> (nil | String) - def create: (String pass) -> untyped - def self.create: (String pass) -> untyped - def self.valid_hash?: (string hash) -> Integer? - def self.verify_password: (untyped pass, untyped hash, ?nil secret) -> untyped - end -end diff --git a/sig/engine.rbs b/sig/engine.rbs new file mode 100644 index 0000000..ab99eab --- /dev/null +++ b/sig/engine.rbs @@ -0,0 +1,6 @@ +# Classes +module Argon2 + class Engine + def self.saltgen: () -> String + end +end diff --git a/sig/ffi_engine.rbs b/sig/ffi_engine.rbs new file mode 100644 index 0000000..4c6227b --- /dev/null +++ b/sig/ffi_engine.rbs @@ -0,0 +1,9 @@ +# Classes +module Argon2 + class Engine + def self.hash_argon2i: (String password, String salt, Integer t_cost, Integer m_cost, ?Integer? out_len) -> String + def self.hash_argon2id: (String password, String salt, Integer t_cost, Integer m_cost, ?Integer? out_len) -> String + def self.hash_argon2id_encode: (String password, String salt, Integer t_cost, Integer m_cost, String? secret) -> String + def self.argon2_verify: (String? pwd, String? hash, String? secret) -> bool + end +end diff --git a/sig/password.rbs b/sig/password.rbs new file mode 100644 index 0000000..2ab1860 --- /dev/null +++ b/sig/password.rbs @@ -0,0 +1,40 @@ +# Classes +module Argon2 + class Password + # Password constants + DEFAULT_T_COST: Integer + MIN_T_COST: Integer + MAX_T_COST: Integer + DEFAULT_M_COST: Integer + MIN_M_COST: Integer + MAX_M_COST: Integer + + # Password attributes + attr_reader digest: String? + attr_reader checksum: String? + attr_reader salt: String? + attr_reader variant: String? + attr_reader version: Integer? + attr_reader t_cost: Integer? + attr_reader m_cost: Integer? + attr_reader p_cost: Integer? + + # Password class methods + def self.create: (String password, ?Hash[Symbol, Integer & String] options) -> instance + def self.valid_hash?: (String digest) -> bool + def self.verify_password: (untyped password, untyped digest, ?String? secret) -> bool + + # Password instance methods + def initialize: (untyped digest) -> instance + def matches?: (untyped password, ?String? secret) -> bool + def ==: (untyped password) -> bool + def to_s: () -> String + def to_str: () -> String + + private + + # Password instance methods (private) + def valid_hash?: (String digest) -> bool + def split_hash: (String digest) -> Hash[Symbol, String & Integer] + end +end From 2def75f86a18009ca34d55731d4a51e721aeb4ef Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Thu, 1 Apr 2021 07:53:51 -0700 Subject: [PATCH 22/35] Update default TEST_CHECKS values Updated the local TEST_CHECKS to 1 to allow faster iterations without needing to pass TEST_CHECKS every time. Also updated the Github action (CI) to use 100 (the original default) so that we still get the value of the NULL hash testing. --- .github/workflows/ruby.yml | 5 ++++- test/rubycheck_test.rb | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 9a531a9..b7e1d00 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -10,12 +10,15 @@ jobs: matrix: ruby-version: ['3.0', 2.7, 2.5] + env: + TEST_CHECKS: 100 + steps: - uses: actions/checkout@v2 - name: Set up Ruby ${{ matrix.ruby-version }} uses: ruby/setup-ruby@v1 with: - ruby-version: ${{ matrix.ruby-version }} + ruby-version: ${{ matrix.ruby-version }} - name: Install dependencies run: bundle install - name: Init submodules diff --git a/test/rubycheck_test.rb b/test/rubycheck_test.rb index 3b351c3..d6eb161 100644 --- a/test/rubycheck_test.rb +++ b/test/rubycheck_test.rb @@ -2,7 +2,8 @@ require 'test_helper' -TIMES = (ENV['TEST_CHECKS'] || 100).to_i +# The Github action sets this to 100, the value used to find the NULL hash bug. +TIMES = (ENV['TEST_CHECKS'] || 1).to_i # This was supposed to use Rubycheck, however the current version doesn't run # These property tests identified the NULL hash bug From 1c10732dd3c867fcf1e5c9f25376d5fd17fe137b Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Thu, 1 Apr 2021 08:00:39 -0700 Subject: [PATCH 23/35] Add error inheritance test --- test/error_test.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/error_test.rb b/test/error_test.rb index 3f8dbe6..bcbcb59 100644 --- a/test/error_test.rb +++ b/test/error_test.rb @@ -28,4 +28,18 @@ def test_passwd_null 16, nil) end end + + def test_error_inheritance + assert Argon2::Error < StandardError + assert Argon2::Errors::InvalidHash < Argon2::Error + assert Argon2::Errors::InvalidVersion < Argon2::Errors::InvalidHash + assert Argon2::Errors::InvalidCost < Argon2::Errors::InvalidHash + assert Argon2::Errors::InvalidTCost < Argon2::Errors::InvalidCost + assert Argon2::Errors::InvalidMCost < Argon2::Errors::InvalidCost + assert Argon2::Errors::InvalidPCost < Argon2::Errors::InvalidCost + assert Argon2::Errors::InvalidPassword < Argon2::Error + assert Argon2::Errors::InvalidSaltSize < Argon2::Error + assert Argon2::Errors::InvalidOutputLength < Argon2::Error + assert Argon2::Errors::ExtError < Argon2::Error + end end From 3d4f484214a15475adaff8018d448f6edb437bd5 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Thu, 1 Apr 2021 08:47:05 -0700 Subject: [PATCH 24/35] Begin adding v3 specs --- test/v3_tests/password_create_test.rb | 86 +++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 test/v3_tests/password_create_test.rb diff --git a/test/v3_tests/password_create_test.rb b/test/v3_tests/password_create_test.rb new file mode 100644 index 0000000..c1917fa --- /dev/null +++ b/test/v3_tests/password_create_test.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'test_helper' + +class PasswordCreate < Minitest::Test + # TODO: Randomly generate a new password with Faker + # SECRET = Faker::Internet.unique.password + SECRET = 'mysecretpassword' + # Time cost constants + DEFAULT_T_COST = Argon2::Password::DEFAULT_T_COST + MIN_T_COST = Argon2::Password::MIN_T_COST + MAX_T_COST = Argon2::Password::MAX_T_COST + # Memory cost constants + DEFAULT_M_COST = Argon2::Password::DEFAULT_M_COST + MIN_M_COST = Argon2::Password::MIN_M_COST + MAX_M_COST = Argon2::Password::MAX_M_COST + + def test_default_t_cost + assert pass = Argon2::Password.create(SECRET, t_cost: DEFAULT_T_COST) + + assert_instance_of Argon2::Password, pass + assert_equal DEFAULT_T_COST, pass.t_cost + end + + def test_min_t_cost + assert pass = Argon2::Password.create(SECRET, t_cost: MIN_T_COST) + + assert_instance_of Argon2::Password, pass + assert_equal MIN_T_COST, pass.t_cost + + # Ensure that going below the minimum results in an InvalidTCost error + assert_raises Argon2::Errors::InvalidTCost do + Argon2::Password.create(SECRET, t_cost: MIN_T_COST - 1) + end + end + + # FIXME: This is a really slow test due to _actually running_ the max cost. Is + # there a way to test that the max cost works without incurring this + # test suite speed penalty? + def test_max_t_cost + assert pass = Argon2::Password.create(SECRET, t_cost: MAX_T_COST) + + assert_instance_of Argon2::Password, pass + assert_equal MAX_T_COST, pass.t_cost + + # Ensure that going above the maximum results in an InvalidTCost error + assert_raises Argon2::Errors::InvalidTCost do + Argon2::Password.create(SECRET, t_cost: MAX_T_COST + 1) + end + end + + def test_default_m_cost + assert pass = Argon2::Password.create(SECRET, m_cost: DEFAULT_M_COST) + + assert_instance_of Argon2::Password, pass + assert_equal DEFAULT_M_COST, pass.m_cost + end + + # FIXME: Failing due to: + # Argon2::Errors::ExtError: ARGON2_MEMORY_TOO_LITTLE + def test_min_m_cost + assert pass = Argon2::Password.create(SECRET, m_cost: MIN_M_COST) + + assert_instance_of Argon2::Password, pass + assert_equal MIN_M_COST, pass.m_cost + + # Ensure that going below the minimum results in an InvalidMCost error + assert_raises Argon2::Errors::InvalidMCost do + Argon2::Password.create(SECRET, m_cost: MIN_M_COST - 1) + end + end + + # FIXME: Failing due to: + # Argon2::Errors::ExtError: ARGON2_MEMORY_ALLOCATION_ERROR + def test_max_m_cost + assert pass = Argon2::Password.create(SECRET, m_cost: MAX_M_COST) + + assert_instance_of Argon2::Password, pass + assert_equal MAX_M_COST, pass.m_cost + + # Ensure that going above the maximum results in an InvalidMCost error + assert_raises Argon2::Errors::InvalidMCost do + Argon2::Password.create(SECRET, m_cost: MAX_M_COST + 1) + end + end +end From 8959d9ccb223e204f968ce5abee76998926413bb Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Thu, 1 Apr 2021 08:49:23 -0700 Subject: [PATCH 25/35] Fix class naming for PasswordCreateTest --- test/v3_tests/password_create_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/v3_tests/password_create_test.rb b/test/v3_tests/password_create_test.rb index c1917fa..4bffbc7 100644 --- a/test/v3_tests/password_create_test.rb +++ b/test/v3_tests/password_create_test.rb @@ -2,7 +2,7 @@ require 'test_helper' -class PasswordCreate < Minitest::Test +class PasswordCreateTest < Minitest::Test # TODO: Randomly generate a new password with Faker # SECRET = Faker::Internet.unique.password SECRET = 'mysecretpassword' From d7425167e01a1e470c646157b491007e98bb8897 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Thu, 1 Apr 2021 08:50:14 -0700 Subject: [PATCH 26/35] Update minimum m_cost to reflect Argon2 actual minimum memory cost --- lib/argon2/password.rb | 2 +- test/v3_tests/password_create_test.rb | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/argon2/password.rb b/lib/argon2/password.rb index 02370c9..8a5b78f 100644 --- a/lib/argon2/password.rb +++ b/lib/argon2/password.rb @@ -14,7 +14,7 @@ class Password # Argon2::Password.create DEFAULT_M_COST = 16 # Used to validate the minimum acceptable memory cost - MIN_M_COST = 1 + MIN_M_COST = 3 # Used to validate the maximum acceptable memory cost MAX_M_COST = 31 # The complete Argon2 digest string (not to be confused with the checksum). diff --git a/test/v3_tests/password_create_test.rb b/test/v3_tests/password_create_test.rb index 4bffbc7..3a72419 100644 --- a/test/v3_tests/password_create_test.rb +++ b/test/v3_tests/password_create_test.rb @@ -56,8 +56,6 @@ def test_default_m_cost assert_equal DEFAULT_M_COST, pass.m_cost end - # FIXME: Failing due to: - # Argon2::Errors::ExtError: ARGON2_MEMORY_TOO_LITTLE def test_min_m_cost assert pass = Argon2::Password.create(SECRET, m_cost: MIN_M_COST) From 7e582723bb29e37273ba522a3121c34796d83504 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Thu, 1 Apr 2021 08:58:04 -0700 Subject: [PATCH 27/35] Also run Github actions against pull requests --- .github/workflows/rubocop.yml | 11 ++++++++--- .github/workflows/ruby.yml | 9 ++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml index 3deeaa7..20d6cd3 100644 --- a/.github/workflows/rubocop.yml +++ b/.github/workflows/rubocop.yml @@ -1,7 +1,13 @@ name: Rubocop -# Run this workflow every time a new commit pushed to your repository -on: push +# Run every time a commit is pushed to master, or a pull request opened against +# master. + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] jobs: @@ -13,4 +19,3 @@ jobs: uses: gimenete/rubocop-action@1.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index b7e1d00..cb6367c 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -1,6 +1,13 @@ name: Test Suite -on: push +# Run every time a commit is pushed to master, or a pull request opened against +# master. + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] jobs: test: From 55a9727d4e29d54737a4e30cec0f099c8ddde74a Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Thu, 1 Apr 2021 09:20:42 -0700 Subject: [PATCH 28/35] Appease da rubocop --- Steepfile | 2 ++ argon2.gemspec | 2 +- lib/argon2/engine.rb | 4 +++ lib/argon2/errors.rb | 2 +- lib/argon2/password.rb | 55 +++++++++++++++++++++--------------------- sig/password.rbs | 2 +- test/api_test.rb | 2 +- 7 files changed, 37 insertions(+), 32 deletions(-) diff --git a/Steepfile b/Steepfile index 01dfa43..981ab7f 100644 --- a/Steepfile +++ b/Steepfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + target :lib do signature "sig" diff --git a/argon2.gemspec b/argon2.gemspec index d3eb332..5eea77d 100644 --- a/argon2.gemspec +++ b/argon2.gemspec @@ -23,7 +23,7 @@ Gem::Specification.new do |spec| 'to follow the "migrating to v3" guide in the project README. Otherwise '\ 'you can safely ignore this message.' - spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } spec.files << `find ext`.split spec.bindir = "exe" diff --git a/lib/argon2/engine.rb b/lib/argon2/engine.rb index 284e894..62087ab 100644 --- a/lib/argon2/engine.rb +++ b/lib/argon2/engine.rb @@ -3,6 +3,10 @@ require 'securerandom' module Argon2 + ## + # The engine class shields users from the FFI interface. + # It is generally not advised to directly use this class. + # class Engine ## # Generates a random, binary string for use as a salt. diff --git a/lib/argon2/errors.rb b/lib/argon2/errors.rb index 628d394..7e6729a 100644 --- a/lib/argon2/errors.rb +++ b/lib/argon2/errors.rb @@ -52,7 +52,7 @@ class InvalidPCost < InvalidCost; end # Raised when a non-string object is passed to Argon2::Password.create # class InvalidPassword < Argon2::Error - def initialize(msg="Invalid password (expected a String)") + def initialize(msg = "Invalid password (expected a String)") super end end diff --git a/lib/argon2/password.rb b/lib/argon2/password.rb index 8a5b78f..f25e261 100644 --- a/lib/argon2/password.rb +++ b/lib/argon2/password.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Argon2 ## # Front-end API for the Argon2 module. @@ -61,13 +63,9 @@ def create(password, options = {}) t_cost = options[:t_cost] || DEFAULT_T_COST m_cost = options[:m_cost] || DEFAULT_M_COST - if t_cost < MIN_T_COST || t_cost > MAX_T_COST - raise Argon2::Errors::InvalidTCost - end + raise Argon2::Errors::InvalidTCost if t_cost < MIN_T_COST || t_cost > MAX_T_COST - if m_cost < MIN_M_COST || m_cost > MAX_M_COST - raise Argon2::Errors::InvalidMCost - end + raise Argon2::Errors::InvalidMCost if m_cost < MIN_M_COST || m_cost > MAX_M_COST # TODO: Add support for changing the p_cost @@ -122,24 +120,20 @@ def verify_password(password, digest, secret = nil) # def initialize(digest) digest = digest.to_s - if valid_hash?(digest) - # Split the digest into its component pieces - split_digest = split_hash(digest) - # Assign each piece to the Argon2::Password instance - @digest = digest - @variant = split_digest[:variant] - @version = split_digest[:version] - @t_cost = split_digest[:t_cost] - @m_cost = split_digest[:m_cost] - @p_cost = split_digest[:p_cost] - @salt = split_digest[:salt] - @checksum = split_digest[:checksum] - # The return type is ignored by Object.new, this is provided purely for - # return type safety (rbs). - self - else - raise Argon2::Errors::InvalidHash - end + + raise Argon2::Errors::InvalidHash unless valid_hash?(digest) + + # Split the digest into its component pieces + split_digest = split_hash(digest) + # Assign each piece to the Argon2::Password instance + @digest = digest + @variant = split_digest[:variant] + @version = split_digest[:version] + @t_cost = split_digest[:t_cost] + @m_cost = split_digest[:m_cost] + @p_cost = split_digest[:p_cost] + @salt = split_digest[:salt] + @checksum = split_digest[:checksum] end ## @@ -154,16 +148,16 @@ def matches?(password, secret = nil) # Compares two Argon2::Password instances to see if they come from the same # digest/hash. # - def ==(password) + def ==(other) # TODO: Should this return false instead of raising an error? - unless password.is_a?(Argon2::Password) + unless other.is_a?(Argon2::Password) raise ArgumentError, - 'Can only compare an Argon2::Password against another Argon2::Password' + 'Can only compare an Argon2::Password against another Argon2::Password' end # TODO: Use secure compare to protect against timing attacks? Also, should # this comparison be more strict? - self.digest == password.digest + digest == other.digest end ## @@ -189,6 +183,9 @@ def valid_hash?(digest) self.class.valid_hash?(digest) end + # FIXME: Reduce complexity/AbcSize + # rubocop:disable Metrics/AbcSize + ## # Helper method to extract the various values from a digest into attributes. # @@ -206,6 +203,7 @@ def split_hash(digest) raise Argon2::Errors::InvalidTCost if t_cost.nil? raise Argon2::Errors::InvalidMCost if m_cost.nil? raise Argon2::Errors::InvalidPCost if p_cost.nil? + # Undo the 2^m_cost operation when encoding the hash to get the original # m_cost input back. m_cost = Math.log2(m_cost[1].to_i).to_i @@ -220,5 +218,6 @@ def split_hash(digest) checksum: checksum.to_str } end + # rubocop:enable Metrics/AbcSize end end diff --git a/sig/password.rbs b/sig/password.rbs index 2ab1860..4c5b20e 100644 --- a/sig/password.rbs +++ b/sig/password.rbs @@ -25,7 +25,7 @@ module Argon2 def self.verify_password: (untyped password, untyped digest, ?String? secret) -> bool # Password instance methods - def initialize: (untyped digest) -> instance + def initialize: (untyped digest) -> void def matches?: (untyped password, ?String? secret) -> bool def ==: (untyped password) -> bool def to_s: () -> String diff --git a/test/api_test.rb b/test/api_test.rb index 6d52112..22c8f18 100644 --- a/test/api_test.rb +++ b/test/api_test.rb @@ -34,7 +34,7 @@ def test_secret end def test_hash - assert pass = Argon2::Password.create('mypassword') + assert Argon2::Password.create('mypassword') end def test_valid_hash From 767a90527b4e791092db82606190877f814b81f0 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Thu, 1 Apr 2021 09:35:26 -0700 Subject: [PATCH 29/35] Fix rubocop Github action being broken --- .github/workflows/rubocop.yml | 21 --------------------- .github/workflows/ruby.yml | 30 ++++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 23 deletions(-) delete mode 100644 .github/workflows/rubocop.yml diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml deleted file mode 100644 index 20d6cd3..0000000 --- a/.github/workflows/rubocop.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Rubocop - -# Run every time a commit is pushed to master, or a pull request opened against -# master. - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - - rubocop: - name: Rubocopchecks - runs-on: ubuntu-latest - steps: - - name: Run Rubocop - uses: gimenete/rubocop-action@1.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index cb6367c..af2e1e4 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -10,12 +10,16 @@ on: branches: [ master ] jobs: - test: + test_matrix: runs-on: ubuntu-latest strategy: matrix: - ruby-version: ['3.0', 2.7, 2.5] + ruby-version: + - 2.5 + - 2.6 + - 2.7 + - 3.0 env: TEST_CHECKS: 100 @@ -40,3 +44,25 @@ jobs: uses: coverallsapp/github-action@master with: github-token: ${{ secrets.GITHUB_TOKEN }} + + rubocop: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.0 + - name: Install dependencies + run: bundle install + - name: Run rubocop + run: bundle exec rubocop + + test: + runs-on: ubuntu-latest + needs: [test_matrix, rubocop] + steps: + - name: Wait for status checks + run: echo "All Green!" From b32caadfc1148eb54958bb86d416cc9e9cf8471e Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Thu, 1 Apr 2021 09:48:02 -0700 Subject: [PATCH 30/35] Remove TODO for Argon2::Password#== The timing attack concern seems to be relatively small, due to the reasoning on: https://github.com/bcrypt-ruby/bcrypt-ruby/pull/43 Also, the Argon2::Password instance thoroughly validates and is created from the digest, so using additional comparisons seems inefficient and unnecessary. Comparing the digest of each password instance should be sufficient. --- lib/argon2/password.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/argon2/password.rb b/lib/argon2/password.rb index f25e261..5b789d6 100644 --- a/lib/argon2/password.rb +++ b/lib/argon2/password.rb @@ -155,8 +155,6 @@ def ==(other) 'Can only compare an Argon2::Password against another Argon2::Password' end - # TODO: Use secure compare to protect against timing attacks? Also, should - # this comparison be more strict? digest == other.digest end From 3a0b69b637afd68a691104d326c24676046f1a1d Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Thu, 1 Apr 2021 10:43:40 -0700 Subject: [PATCH 31/35] Disable actually running max cost hashes in specs (for now) --- lib/argon2/password.rb | 1 - test/v3_tests/password_create_test.rb | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/argon2/password.rb b/lib/argon2/password.rb index 5b789d6..7af3ea2 100644 --- a/lib/argon2/password.rb +++ b/lib/argon2/password.rb @@ -64,7 +64,6 @@ def create(password, options = {}) m_cost = options[:m_cost] || DEFAULT_M_COST raise Argon2::Errors::InvalidTCost if t_cost < MIN_T_COST || t_cost > MAX_T_COST - raise Argon2::Errors::InvalidMCost if m_cost < MIN_M_COST || m_cost > MAX_M_COST # TODO: Add support for changing the p_cost diff --git a/test/v3_tests/password_create_test.rb b/test/v3_tests/password_create_test.rb index 3a72419..95eaddb 100644 --- a/test/v3_tests/password_create_test.rb +++ b/test/v3_tests/password_create_test.rb @@ -38,10 +38,10 @@ def test_min_t_cost # there a way to test that the max cost works without incurring this # test suite speed penalty? def test_max_t_cost - assert pass = Argon2::Password.create(SECRET, t_cost: MAX_T_COST) + # assert pass = Argon2::Password.create(SECRET, t_cost: MAX_T_COST) - assert_instance_of Argon2::Password, pass - assert_equal MAX_T_COST, pass.t_cost + # assert_instance_of Argon2::Password, pass + # assert_equal MAX_T_COST, pass.t_cost # Ensure that going above the maximum results in an InvalidTCost error assert_raises Argon2::Errors::InvalidTCost do @@ -68,13 +68,13 @@ def test_min_m_cost end end - # FIXME: Failing due to: + # FIXME: When testing maximum memory cost, fails due to: # Argon2::Errors::ExtError: ARGON2_MEMORY_ALLOCATION_ERROR def test_max_m_cost - assert pass = Argon2::Password.create(SECRET, m_cost: MAX_M_COST) + # assert pass = Argon2::Password.create(SECRET, m_cost: MAX_M_COST) - assert_instance_of Argon2::Password, pass - assert_equal MAX_M_COST, pass.m_cost + # assert_instance_of Argon2::Password, pass + # assert_equal MAX_M_COST, pass.m_cost # Ensure that going above the maximum results in an InvalidMCost error assert_raises Argon2::Errors::InvalidMCost do From a79bc0d8187ab1349ed35a6b5afd70cf6296b5b2 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Thu, 1 Apr 2021 11:08:31 -0700 Subject: [PATCH 32/35] Bring test coverage back to 100% --- test/error_test.rb | 5 +++++ test/v3_tests/password_equals_test.rb | 25 +++++++++++++++++++++ test/v3_tests/password_matches_test.rb | 31 ++++++++++++++++++++++++++ test/v3_tests/verify_password_test.rb | 31 ++++++++++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 test/v3_tests/password_equals_test.rb create mode 100644 test/v3_tests/password_matches_test.rb create mode 100644 test/v3_tests/verify_password_test.rb diff --git a/test/error_test.rb b/test/error_test.rb index bcbcb59..d5355a1 100644 --- a/test/error_test.rb +++ b/test/error_test.rb @@ -42,4 +42,9 @@ def test_error_inheritance assert Argon2::Errors::InvalidOutputLength < Argon2::Error assert Argon2::Errors::ExtError < Argon2::Error end + + def test_invalid_password_msg + assert_equal Argon2::Errors::InvalidPassword.new.message, + "Invalid password (expected a String)" + end end diff --git a/test/v3_tests/password_equals_test.rb b/test/v3_tests/password_equals_test.rb new file mode 100644 index 0000000..c4bbe98 --- /dev/null +++ b/test/v3_tests/password_equals_test.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'test_helper' + +class PasswordEqualsTest < Minitest::Test + # TODO: Randomly generate a new password with Faker + # SECRET = Faker::Internet.unique.password + SECRET = 'mysecretpassword' + PASS = Argon2::Password.create(SECRET) + DIGEST = PASS.to_s + + def test_refuses_string + assert_raises ArgumentError do + PASS == SECRET + end + + assert_raises ArgumentError do + PASS == DIGEST + end + end + + def test_accepts_argon2_password + assert_equal Argon2::Password.new(DIGEST), PASS + end +end diff --git a/test/v3_tests/password_matches_test.rb b/test/v3_tests/password_matches_test.rb new file mode 100644 index 0000000..61d7d9a --- /dev/null +++ b/test/v3_tests/password_matches_test.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'test_helper' + +class PasswordMatchesTest < Minitest::Test + # TODO: Randomly generate a new password with Faker + # SECRET = Faker::Internet.unique.password + SECRET = 'mysecretpassword' + PASS = Argon2::Password.create(SECRET) + DIGEST = PASS.to_s + # Confirm other values are invalid + OTHER_SECRET = 'notmysecretpassword' + OTHER_PASS = Argon2::Password.create(OTHER_SECRET) + OTHER_DIGEST = OTHER_PASS.to_s + + def test_accepts_string + assert PASS.matches?(SECRET) + assert OTHER_PASS.matches?(OTHER_SECRET) + + refute PASS.matches?(OTHER_SECRET) + refute OTHER_PASS.matches?(SECRET) + end + + def test_accepts_argon2_password + assert PASS.matches?(PASS) + assert OTHER_PASS.matches?(OTHER_PASS) + + refute PASS.matches?(OTHER_PASS) + refute OTHER_PASS.matches?(PASS) + end +end diff --git a/test/v3_tests/verify_password_test.rb b/test/v3_tests/verify_password_test.rb new file mode 100644 index 0000000..d33cbb9 --- /dev/null +++ b/test/v3_tests/verify_password_test.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'test_helper' + +class VerifyPasswordTest < Minitest::Test + # TODO: Randomly generate a new password with Faker + # SECRET = Faker::Internet.unique.password + SECRET = 'mysecretpassword' + PASS = Argon2::Password.create(SECRET) + DIGEST = PASS.to_s + # Confirm other values are invalid + OTHER_SECRET = 'notmysecretpassword' + OTHER_PASS = Argon2::Password.create(OTHER_SECRET) + OTHER_DIGEST = OTHER_PASS.to_s + + def test_accepts_string + assert Argon2::Password.verify_password(SECRET, DIGEST) + assert Argon2::Password.verify_password(OTHER_SECRET, OTHER_DIGEST) + + refute Argon2::Password.verify_password(SECRET, OTHER_DIGEST) + refute Argon2::Password.verify_password(OTHER_SECRET, DIGEST) + end + + def test_accepts_argon2_password + assert Argon2::Password.verify_password(PASS, DIGEST) + assert Argon2::Password.verify_password(OTHER_PASS, OTHER_DIGEST) + + refute Argon2::Password.verify_password(PASS, OTHER_DIGEST) + refute Argon2::Password.verify_password(OTHER_PASS, DIGEST) + end +end From 18d405e4efad521b7802a6be830ed3a76756114a Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Thu, 1 Apr 2021 11:30:05 -0700 Subject: [PATCH 33/35] Update github actions to run against everything --- .github/workflows/ruby.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index af2e1e4..f6e22bd 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -1,13 +1,7 @@ name: Test Suite -# Run every time a commit is pushed to master, or a pull request opened against -# master. - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] +# Run against all commits and pull requests. +on: [push, pull_request] jobs: test_matrix: From a931fbbaf4333b721b5238a07a284fe81c8b6687 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Thu, 1 Apr 2021 11:30:25 -0700 Subject: [PATCH 34/35] Update Coveralls to run in parallel See: https://github.com/marketplace/actions/coveralls-github-action#complete-parallel-job-example --- .github/workflows/ruby.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index f6e22bd..bd7559c 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -34,10 +34,12 @@ jobs: run: cd ext/argon2_wrap/ && make test && cd ../.. - name: Run tests run: bundle exec rake - - name: Coveralls + - name: Coveralls Parallel uses: coverallsapp/github-action@master with: github-token: ${{ secrets.GITHUB_TOKEN }} + flag-name: run-${{ matrix.ruby-version }} + parallel: true rubocop: @@ -54,9 +56,14 @@ jobs: - name: Run rubocop run: bundle exec rubocop - test: + finish: runs-on: ubuntu-latest needs: [test_matrix, rubocop] steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true - name: Wait for status checks run: echo "All Green!" From ed99f4fd881c270ef01c28293715a48dfbfb5316 Mon Sep 17 00:00:00 2001 From: Josh Buker Date: Thu, 8 Apr 2021 10:54:14 -0700 Subject: [PATCH 35/35] Add additional tests, and minor meta updates * Added some new tests * Updated default rake task to run both test and rubocop * Update SimpleCov to not test the coverage of the minitest suite itself --- .github/workflows/ruby.yml | 4 +- Rakefile | 2 +- test/api_test.rb | 36 +++++++++++++++ test/key_test.rb | 63 +++++++++++++++++++++++++++ test/low_level_test.rb | 1 + test/test_helper.rb | 5 ++- test/unicode_test.rb | 2 + test/v3_tests/password_new_test.rb | 43 ++++++++++++++++++ test/v3_tests/verify_password_test.rb | 20 ++++++++- 9 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 test/v3_tests/password_new_test.rb diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index bd7559c..632570d 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -33,7 +33,7 @@ jobs: - name: Test C library run: cd ext/argon2_wrap/ && make test && cd ../.. - name: Run tests - run: bundle exec rake + run: bundle exec rake test - name: Coveralls Parallel uses: coverallsapp/github-action@master with: @@ -54,7 +54,7 @@ jobs: - name: Install dependencies run: bundle install - name: Run rubocop - run: bundle exec rubocop + run: bundle exec rake rubocop finish: runs-on: ubuntu-latest diff --git a/Rakefile b/Rakefile index 98db72a..374cf29 100644 --- a/Rakefile +++ b/Rakefile @@ -13,4 +13,4 @@ Rake::TestTask.new(:test) do |t| t.test_files = FileList['test/**/*_test.rb'] end -task :default => :test +task :default => %i[test rubocop] diff --git a/test/api_test.rb b/test/api_test.rb index 22c8f18..c5b655a 100644 --- a/test/api_test.rb +++ b/test/api_test.rb @@ -41,4 +41,40 @@ def test_valid_hash secure_pass = Argon2::Password.create('A secret') assert Argon2::Password.valid_hash?(secure_pass) end + + ############### + ## New Tests ## + ############### + ORIGINAL_PASSWORD = 'mypassword' + PEPPER = 'A secret' + + def test_create_password_without_parameters + assert argon2 = Argon2::Password.create(ORIGINAL_PASSWORD) + + assert argon2.is_a?(Argon2::Password) + assert_equal argon2.m_cost, 16 + assert_equal argon2.t_cost, 2 + assert argon2.matches?(ORIGINAL_PASSWORD) + assert Argon2::Password.verify_password(ORIGINAL_PASSWORD, argon2) + end + + def test_create_password_with_parameters + assert argon2 = Argon2::Password.create(ORIGINAL_PASSWORD, t_cost: 4, m_cost: 12) + + assert argon2.is_a?(Argon2::Password) + assert_equal argon2.m_cost, 12 + assert_equal argon2.t_cost, 4 + assert argon2.matches?(ORIGINAL_PASSWORD) + assert Argon2::Password.verify_password(ORIGINAL_PASSWORD, argon2) + end + + def test_create_password_with_secret + assert argon2 = Argon2::Password.create(ORIGINAL_PASSWORD, secret: PEPPER) + + assert argon2.is_a?(Argon2::Password) + assert_equal argon2.m_cost, 16 + assert_equal argon2.t_cost, 2 + assert argon2.matches?(ORIGINAL_PASSWORD, PEPPER) + assert Argon2::Password.verify_password(ORIGINAL_PASSWORD, argon2, PEPPER) + end end diff --git a/test/key_test.rb b/test/key_test.rb index 3c4b2f4..89dacab 100644 --- a/test/key_test.rb +++ b/test/key_test.rb @@ -10,10 +10,73 @@ def test_key_hash assert basehash = Argon2::Password.create(PASS, t_cost: 2, m_cost: 16) # Keyed hash assert keyhash = Argon2::Password.create(PASS, t_cost: 2, m_cost: 16, secret: KEY) + + # Isn't this test somewhat pointless? Each password will have a new salt, + # so they can never match anyway. refute_equal basehash, keyhash + + # Demonstrate problem: + assert salthash = Argon2::Password.create(PASS, t_cost: 2, m_cost: 16) + refute_equal basehash, salthash + # Prove that it's not just the `==` being broken: + assert_equal basehash, basehash + assert_equal salthash, salthash + assert_equal keyhash, keyhash + # The keyed hash - without the key refute Argon2::Password.verify_password(PASS, keyhash) # With key assert Argon2::Password.verify_password(PASS, keyhash, KEY) end + + # So apparently the salt that's in the digest is actually different from the + # salt used to generate the argon2 hash. + # + # To demonstrate: + # + # salt = Argon2::Engine.saltgen + # argon2 = Argon2::Password.create('anysecret', salt_do_not_supply: salt) + # salt == argon2.salt + # => false + # salt.length + # => 16 + # argon2.salt.length + # => 22 + # salt + # => "\xCA\xFD\xED\x18\x10\xD1!R\xF2\xA9\f4\xD2\x966\x9D" + # argon2.salt + # => "yv3tGBDRIVLyqQw00pY2nQ" + # + # This combined with the fact that you're not supposed to supply the salt in + # the first place, makes me wonder if we should just hard deprecate passing in + # the salt at all. In its current state, it provides literally no value and + # allows developers to shoot themselves in the foot. + # + # This test might be blown away soon anyway, temp disable abc cop. + # rubocop:disable Metrics/AbcSize + def test_key_hash_with_same_salts + skip('The salt going in is not the same as the salt in the digest') + assert basehash = Argon2::Password.create(PASS) + assert salthash = Argon2::Password.create(PASS, salt_do_not_supply: basehash.salt) + assert keyhash = Argon2::Password.create(PASS, salt_do_not_supply: basehash.salt, secret: KEY) + + # Prove that you can create an identical copy + assert_equal basehash, salthash + # Prove that both hashes without peppers do not equal hash with pepper + refute_equal basehash, keyhash + refute_equal salthash, keyhash + + # Test unkeyed hashes + assert Argon2::Password.verify_password(PASS, basehash) + assert Argon2::Password.verify_password(PASS, salthash) + + refute Argon2::Password.verify_password(PASS, basehash, KEY) + refute Argon2::Password.verify_password(PASS, salthash, KEY) + + # Test keyed hash + refute Argon2::Password.verify_password(PASS, keyhash) + + assert Argon2::Password.verify_password(PASS, keyhash, KEY) + end + # rubocop:enable Metrics/AbcSize end diff --git a/test/low_level_test.rb b/test/low_level_test.rb index d62eead..3f42247 100644 --- a/test/low_level_test.rb +++ b/test/low_level_test.rb @@ -7,6 +7,7 @@ class LowLevelArgon2Test < Minitest::Test def test_that_it_has_a_version_number refute_nil ::Argon2::VERSION + assert ::Argon2::VERSION.is_a?(String) end def test_ffi_vector diff --git a/test/test_helper.rb b/test/test_helper.rb index c5490da..6b89aba 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -11,7 +11,10 @@ c.single_report_path = 'coverage/lcov.info' end SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter -SimpleCov.start +SimpleCov.start do + # Don't test the coverage of the test suite itself... + add_filter '/test' +end require 'argon2' diff --git a/test/unicode_test.rb b/test/unicode_test.rb index d406280..5ebdb52 100644 --- a/test/unicode_test.rb +++ b/test/unicode_test.rb @@ -24,5 +24,7 @@ def test_emoji hash = Argon2::Password.create(rawstr) assert Argon2::Password.verify_password(rawstr, hash) refute Argon2::Password.verify_password("", hash) + # Also test if emoji are stripped but spaces remained. + refute Argon2::Password.verify_password(" ", hash) end end diff --git a/test/v3_tests/password_new_test.rb b/test/v3_tests/password_new_test.rb new file mode 100644 index 0000000..975b016 --- /dev/null +++ b/test/v3_tests/password_new_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'test_helper' + +class PasswordNewTest < Minitest::Test + # TODO: Randomly generate a new password with Faker + # SECRET = Faker::Internet.unique.password + SECRET = 'mysecretpassword' + PASS = Argon2::Password.create(SECRET) + DIGEST = PASS.to_s + # What would be appropriate bad digest(s) to test? Using a bcrypt hash for now + BAD_DIGEST = '$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW' + + def test_accepts_valid_string + assert argon2 = Argon2::Password.new(DIGEST) + assert argon2.is_a?(Argon2::Password) + + assert_equal argon2, PASS + assert_equal argon2.to_s, DIGEST + + assert argon2.matches?(SECRET) + assert Argon2::Password.verify_password(SECRET, argon2) + assert Argon2::Password.verify_password(argon2, DIGEST) + end + + def test_rejects_invalid_string + assert_raises Argon2::Errors::InvalidHash do + Argon2::Password.new(BAD_DIGEST) + end + end + + def test_accepts_argon2_password + assert argon2 = Argon2::Password.new(PASS) + assert argon2.is_a?(Argon2::Password) + + assert_equal argon2, PASS + assert_equal argon2.to_s, DIGEST + + assert argon2.matches?(SECRET) + assert Argon2::Password.verify_password(SECRET, argon2) + assert Argon2::Password.verify_password(argon2, DIGEST) + end +end diff --git a/test/v3_tests/verify_password_test.rb b/test/v3_tests/verify_password_test.rb index d33cbb9..a35b4a8 100644 --- a/test/v3_tests/verify_password_test.rb +++ b/test/v3_tests/verify_password_test.rb @@ -13,7 +13,7 @@ class VerifyPasswordTest < Minitest::Test OTHER_PASS = Argon2::Password.create(OTHER_SECRET) OTHER_DIGEST = OTHER_PASS.to_s - def test_accepts_string + def test_accepts_string_as_secret assert Argon2::Password.verify_password(SECRET, DIGEST) assert Argon2::Password.verify_password(OTHER_SECRET, OTHER_DIGEST) @@ -21,11 +21,27 @@ def test_accepts_string refute Argon2::Password.verify_password(OTHER_SECRET, DIGEST) end - def test_accepts_argon2_password + def test_accepts_argon2_password_as_secret assert Argon2::Password.verify_password(PASS, DIGEST) assert Argon2::Password.verify_password(OTHER_PASS, OTHER_DIGEST) refute Argon2::Password.verify_password(PASS, OTHER_DIGEST) refute Argon2::Password.verify_password(OTHER_PASS, DIGEST) end + + def test_accepts_argon2_password_as_digest + assert Argon2::Password.verify_password(SECRET, PASS) + assert Argon2::Password.verify_password(OTHER_SECRET, OTHER_PASS) + + refute Argon2::Password.verify_password(SECRET, OTHER_PASS) + refute Argon2::Password.verify_password(OTHER_SECRET, PASS) + end + + def test_accepts_tautology + assert Argon2::Password.verify_password(PASS, PASS) + assert Argon2::Password.verify_password(OTHER_PASS, OTHER_PASS) + + refute Argon2::Password.verify_password(PASS, OTHER_PASS) + refute Argon2::Password.verify_password(OTHER_PASS, PASS) + end end