Skip to content

Refactor Argon2::Password #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 36 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
acbbf92
Clean up README line length
joshbuker Mar 27, 2021
37bcd26
Remove autogenerated comment from Gemfile
joshbuker Mar 27, 2021
fd6b4f3
Clean up yard documentation and move password class to its own file
joshbuker Mar 27, 2021
3d3531b
Refactor errors to allow better exception handling
joshbuker Mar 27, 2021
f8106d0
Add migration to v3 notes for new errors
joshbuker Mar 27, 2021
3abc5d2
Does this markdown formatting even work?
joshbuker Mar 27, 2021
cadaf88
Keep the list styling simple
joshbuker Mar 27, 2021
707a10d
Refactor the Argon2::Password class
joshbuker Mar 28, 2021
bb0bdf0
Update Changelog.md with migration details
joshbuker Mar 28, 2021
a2a9954
Fix Argon2::Password.new breaking when passed another Argon2::Password
joshbuker Mar 28, 2021
4ed16f2
Revert all test suite changes
joshbuker Mar 28, 2021
ff9f2c3
Refactor minitest suite to support new changes
joshbuker Mar 28, 2021
db7e8bc
Revert Gemfile and gemspec changes, nice-to-haves
joshbuker Mar 28, 2021
94ad585
Minimize styling changes
joshbuker Mar 28, 2021
a498dbd
Remove TODOs and update Argon2::Password documentation
joshbuker Mar 28, 2021
d14bb3e
Fix typo
joshbuker Mar 28, 2021
36c1d92
Fix slight misnomer in comments
joshbuker Mar 28, 2021
54edb17
Flesh out YARD documentation
joshbuker Mar 28, 2021
c0e149a
Fix yard thinking the argon variants were a link
joshbuker Mar 28, 2021
3206644
Reach 100% yard documentation, add error docs
joshbuker Mar 28, 2021
1904536
Merge branch 'master' into feature/additional-helpers
joshbuker Mar 29, 2021
5715a52
Update type signatures for new password API (WIP)
joshbuker Mar 29, 2021
2def75f
Update default TEST_CHECKS values
joshbuker Apr 1, 2021
1c10732
Add error inheritance test
joshbuker Apr 1, 2021
3d4f484
Begin adding v3 specs
joshbuker Apr 1, 2021
8959d9c
Fix class naming for PasswordCreateTest
joshbuker Apr 1, 2021
d742516
Update minimum m_cost to reflect Argon2 actual minimum memory cost
joshbuker Apr 1, 2021
7e58272
Also run Github actions against pull requests
joshbuker Apr 1, 2021
55a9727
Appease da rubocop
joshbuker Apr 1, 2021
767a905
Fix rubocop Github action being broken
joshbuker Apr 1, 2021
b32caad
Remove TODO for Argon2::Password#==
joshbuker Apr 1, 2021
3a0b69b
Disable actually running max cost hashes in specs (for now)
joshbuker Apr 1, 2021
a79bc0d
Bring test coverage back to 100%
joshbuker Apr 1, 2021
18d405e
Update github actions to run against everything
joshbuker Apr 1, 2021
a931fbb
Update Coveralls to run in parallel
joshbuker Apr 1, 2021
ed99f4f
Add additional tests, and minor meta updates
joshbuker Apr 8, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions .github/workflows/rubocop.yml

This file was deleted.

49 changes: 43 additions & 6 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
name: Test Suite

on: push
# Run against all commits and pull requests.
on: [push, pull_request]

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

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
Expand All @@ -25,8 +33,37 @@ jobs:
- name: Test C library
run: cd ext/argon2_wrap/ && make test && cd ../..
- name: Run tests
run: bundle exec rake
- name: Coveralls
run: bundle exec rake test
- name: Coveralls Parallel
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
flag-name: run-${{ matrix.ruby-version }}
parallel: true

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 rake rubocop

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!"
12 changes: 12 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# Changelog

## v3.0.0: TBD

**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.

Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ 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)
```

## Ruby 3 Types

I am now shipping signatures in sig/. The following command sets up a testing interface.
```sh
RBS_TEST_TARGET="Argon2::*" bundle exec ruby -r rbs/test/setup bin/console
Expand All @@ -83,6 +85,45 @@ steep check
```
These tools will need to be installed manually at this time and will be added to Gemfiles after much further testing.

## Version 3.0 - Breaking API changes, addition of new helper functions

### Argon2::Password API refactored

**Argon2::Password.new and Argon2::Password.create are now different.**

Argon2::Passwords can now be created without initializing an instance first.

```ruby
# 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)
```

### Errors restructured

**The root level error for Argon2 has been renamed.**

Argon2::ArgonHashFail has been renamed to Argon2::Error

```ruby
# 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
```

## 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.

Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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]
2 changes: 2 additions & 0 deletions Steepfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

target :lib do
signature "sig"

Expand Down
9 changes: 8 additions & 1 deletion argon2.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ Gem::Specification.new do |spec|
spec.homepage = 'https://github.com/technion/ruby-argon2'
spec.license = 'MIT'

spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
# 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

spec.bindir = "exe"
Expand Down
51 changes: 10 additions & 41 deletions lib/argon2.rb
Original file line number Diff line number Diff line change
@@ -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'
8 changes: 4 additions & 4 deletions lib/argon2/constants.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# 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
SALT_LEN = 16 # Standard recommendation from the Argon2 spec
OUT_LEN = 32 # Binary, unencoded output
ENCODE_LEN = 108 # Encoded output
end
end
8 changes: 7 additions & 1 deletion lib/argon2/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
require 'securerandom'

module Argon2
# Generates a random, binary string for use as a salt.
##
# 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.
#
def self.saltgen
SecureRandom.random_bytes(Argon2::Constants::SALT_LEN)
end
Expand Down
87 changes: 82 additions & 5 deletions lib/argon2/errors.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,87 @@
# 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
class ArgonHashFail < StandardError; end
##
# Generic error to catch anything the Argon2 ruby library throws.
#
class Error < StandardError; end

##
# Various errors for invalid parameters passed to the library.
#
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

##
# 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

##
# 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. Any
# error from this list will be thrown as an Argon2::Errors::ExtError
#
ERRORS = %w[
ARGON2_OK
ARGON2_OUTPUT_PTR_NULL
Expand Down Expand Up @@ -40,5 +117,5 @@ class ArgonHashFail < StandardError; end
ARGON2_ENCODING_FAIL
ARGON2_DECODING_FAIL
ARGON2_THREAD_FAIL
].freeze
].freeze
end
Loading