Skip to content

Commit

Permalink
Add JRuby support via JRuby-OpenSSL
Browse files Browse the repository at this point in the history
As JRuby doesn't support the C extension, use JRuby-OpenSSL's inclusion
of Bouncy Castle to re-implement the libargon2 interface using
Argon2BytesGenerator.

In order for the C and Java versions to be consistent, we now raise an
ArgumentError for any invalid encoded hashes when verifying though the
exact error messages returned will differ based on the underlying
library (so those arguably-over-specific assertions have been removed).

In order to implement constant-time verification, we now also extract
the output of each encoded password hash.
  • Loading branch information
mudge committed Nov 2, 2024
1 parent 21a9fc3 commit cf6c8b4
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ruby: ["2.6", "2.7", "3.0", "3.1", "3.2", "3.3", "3.4", "truffleruby"]
ruby: ["2.6", "2.7", "3.0", "3.1", "3.2", "3.3", "3.4", "truffleruby", "jruby-9.4"]
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v4
Expand Down
3 changes: 3 additions & 0 deletions ext/argon2id/argon2id.c
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ rb_argon2id_verify(VALUE module, VALUE encoded, VALUE pwd) {
if (result == ARGON2_VERIFY_MISMATCH) {
return Qfalse;
}
if (result == ARGON2_DECODING_FAIL || result == ARGON2_DECODING_LENGTH_FAIL) {
rb_raise(rb_eArgError, "%s", argon2_error_message(result));
}

rb_raise(cArgon2idError, "%s", argon2_error_message(result));
}
Expand Down
50 changes: 45 additions & 5 deletions lib/argon2id.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
# frozen_string_literal: true

begin
::RUBY_VERSION =~ /(\d+\.\d+)/
require_relative "#{Regexp.last_match(1)}/argon2id.so"
rescue LoadError
require "argon2id.so"
unless RUBY_PLATFORM == "java"
begin
::RUBY_VERSION =~ /(\d+\.\d+)/
require_relative "#{Regexp.last_match(1)}/argon2id.so"
rescue LoadError
require "argon2id.so"
end
end

require "argon2id/version"
require "argon2id/password"

module Argon2id
Error = Class.new(StandardError) if RUBY_PLATFORM == "java"

# The default "time cost" of 2 iterations recommended by OWASP.
DEFAULT_T_COST = 2

Expand Down Expand Up @@ -47,5 +51,41 @@ class << self

# The default desired length of the hash in bytes used by Argon2id::Password.create
attr_accessor :output_len

if RUBY_PLATFORM == "java"
require "openssl"

def hash_encoded(t_cost, m_cost, parallelism, pwd, salt, hashlen)
raise Error, "Salt is too short" unless String(salt).bytesize.positive?

hash = Java::byte[Integer(hashlen)].new
params = Java::OrgBouncycastleCryptoParams::Argon2Parameters::Builder.new(Java::OrgBouncycastleCryptoParams::Argon2Parameters::ARGON2_id)
.with_salt(salt.to_java_bytes)
.with_parallelism(Integer(parallelism))
.with_memory_as_kb(Integer(m_cost))
.with_iterations(Integer(t_cost))
.build
generator = Java::OrgBouncycastleCryptoGenerators::Argon2BytesGenerator.new
encoder = Java::JavaUtil::Base64.get_encoder.without_padding

generator.init(params)
generator.generate_bytes(pwd.to_java_bytes, hash)

encoded_salt = encoder.encode_to_string(params.get_salt)
encoded_output = encoder.encode_to_string(hash)

"$argon2id$v=#{params.get_version}$m=#{params.get_memory},t=#{params.get_iterations},p=#{params.get_lanes}$#{encoded_salt}$#{encoded_output}"
rescue => e
raise Error, e.message
end

def verify(encoded, pwd)
password = Password.new(encoded)

other_password = Password.new(hash_encoded(password.t_cost, password.m_cost, password.parallelism, String(pwd), password.salt, password.output.bytesize))

Java::OrgBouncycastleUtil::Arrays.constant_time_are_equal(password.output.to_java_bytes, other_password.output.to_java_bytes)
end
end
end
end
8 changes: 5 additions & 3 deletions lib/argon2id/password.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class Password
\$
([a-zA-Z0-9+/]+)
\$
[a-zA-Z0-9+/]+
([a-zA-Z0-9+/]+)
\z
}x.freeze

Expand All @@ -69,6 +69,9 @@ class Password
# The salt.
attr_reader :salt

# The hash output.
attr_reader :output

# Create a new Password object that hashes a given plain text password +pwd+.
#
# - +:t_cost+: integer (default 2) the "time cost" given as a number of iterations
Expand Down Expand Up @@ -101,8 +104,6 @@ def self.create(pwd, t_cost: Argon2id.t_cost, m_cost: Argon2id.m_cost, paralleli
)
end

# call-seq: Argon2id::Password.new(encoded)
#
# Create a new Password with the given encoded password hash.
#
# password = Argon2id::Password.new("$argon2id$v=19$m=19456,t=2,p=1$FI8yp1gXbthJCskBlpKPoQ$nOfCCpS2r+I8GRN71cZND4cskn7YKBNzuHUEO3YpY2s")
Expand All @@ -118,6 +119,7 @@ def initialize(encoded)
@t_cost = Integer($4)
@parallelism = Integer($5)
@salt = $6.unpack1("m")
@output = $7.unpack1("m")
end

# Return the encoded password hash.
Expand Down
22 changes: 6 additions & 16 deletions test/test_hash_encoded.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,42 +17,32 @@ def test_valid_password_does_not_include_trailing_null_byte
end

def test_raises_with_too_short_output
error = assert_raises(Argon2id::Error) do
assert_raises(Argon2id::Error) do
Argon2id.hash_encoded(2, 256, 1, "password", "somesalt", 1)
end

assert_equal "Output is too short", error.message
end

def test_raises_with_too_few_lanes
error = assert_raises(Argon2id::Error) do
assert_raises(Argon2id::Error) do
Argon2id.hash_encoded(2, 256, 0, "password", "somesalt", 32)
end

assert_equal "Too few lanes", error.message
end

def test_raises_with_too_small_memory_cost
error = assert_raises(Argon2id::Error) do
assert_raises(Argon2id::Error) do
Argon2id.hash_encoded(2, 0, 1, "password", "somesalt", 32)
end

assert_equal "Memory cost is too small", error.message
end

def test_raises_with_too_small_time_cost
error = assert_raises(Argon2id::Error) do
assert_raises(Argon2id::Error) do
Argon2id.hash_encoded(0, 256, 1, "password", "somesalt", 32)
end

assert_equal "Time cost is too small", error.message
end

def test_raises_with_too_short_salt
error = assert_raises(Argon2id::Error) do
Argon2id.hash_encoded(0, 256, 1, "password", "", 32)
assert_raises(Argon2id::Error) do
Argon2id.hash_encoded(2, 256, 1, "password", "", 32)
end

assert_equal "Salt is too short", error.message
end
end
2 changes: 1 addition & 1 deletion test/test_verify.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def test_returns_false_with_incorrect_password
end

def test_raises_if_given_invalid_encoded
assert_raises(Argon2id::Error) do
assert_raises(ArgumentError) do
Argon2id.verify("", "opensesame")
end
end
Expand Down

0 comments on commit cf6c8b4

Please sign in to comment.