Skip to content

[fix] add Cipher#auth_data(arg) override (Rails 7.x compat) #291

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

Merged
merged 4 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 9 additions & 5 deletions .github/workflows/ci-test.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: JOSSL Tests
name: rake test

on: [push, pull_request]

Expand All @@ -13,15 +13,19 @@ jobs:

strategy:
matrix:
java-version: [ 8, 11, 15, 17 ]
ruby-version: [ jruby-9.3.3.0 ]
java-version: [ 8, 11, 17, 21 ]
ruby-version: [ jruby-9.4.5.0 ]
include:
- java-version: 8
ruby-version: jruby-9.2.19.0
- java-version: 11
ruby-version: jruby-9.2.20.1
- java-version: 17
ruby-version: jruby-9.2.20.1
- java-version: 8
ruby-version: jruby-9.3.3.0
- java-version: 11
ruby-version: jruby-9.3.13.0
- java-version: 15
ruby-version: jruby-9.3.13.0
- java-version: 8
ruby-version: jruby-9.1.17.0
fail-fast: false
Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/ci-test_provider.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: JOSSL Tests (Provider)
name: rake test (with provider)

on: [push, pull_request]

Expand All @@ -9,12 +9,15 @@ env:
jobs:

maven-test:
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04

strategy:
matrix:
java-version: [ 11, 17 ]
ruby-version: [ jruby-9.3.3.0 ]
ruby-version: [ jruby-9.3.13.0 ]
include:
- java-version: 21
ruby-version: jruby-9.4.5.0
fail-fast: false

steps:
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ gem 'jar-dependencies', '>= 0.3.11', require: false
gem 'mocha', '~> 1.4', '< 2.0'

# NOTE: runit-maven-plugin will use it's own :
gem 'test-unit', '2.5.5'
gem 'test-unit'
38 changes: 21 additions & 17 deletions src/main/java/org/jruby/ext/openssl/Cipher.java
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,6 @@ public Cipher(Ruby runtime, RubyClass type) {
private int generateKeyLength = -1;
private int ivLength = -1;
private boolean encryptMode = true;
//private IRubyObject[] modeParams;
private boolean cipherInited = false;
private byte[] key;
private byte[] realIV;
Expand Down Expand Up @@ -1265,11 +1264,24 @@ public IRubyObject set_padding(IRubyObject padding) {
}

private transient ByteList auth_tag;
private int auth_tag_len = 16;

@JRubyMethod(name = "auth_tag")
public IRubyObject auth_tag(final ThreadContext context) {
return getAuthTag(context, auth_tag_len);
}

@JRubyMethod(name = "auth_tag")
public IRubyObject auth_tag(final ThreadContext context, IRubyObject tag_len) {
return getAuthTag(context, tag_len.convertToInteger().getIntValue());
}

private IRubyObject getAuthTag(final ThreadContext context, final int tag_len) {
if ( auth_tag != null ) {
return RubyString.newString(context.runtime, auth_tag);
if (auth_tag.length() <= tag_len) {
return RubyString.newString(context.runtime, auth_tag);
}
return RubyString.newString(context.runtime, (ByteList) auth_tag.subSequence(0, tag_len));
}
if ( ! isAuthDataMode() ) {
throw newCipherError(context.runtime, "authentication tag not supported by this cipher");
Expand All @@ -1287,14 +1299,18 @@ public IRubyObject set_auth_tag(final ThreadContext context, final IRubyObject t
return auth_tag;
}

@JRubyMethod(name = "auth_tag_len=")
public IRubyObject set_auth_tag_len(IRubyObject tag_len) {
this.auth_tag_len = tag_len.convertToInteger().getIntValue();
return tag_len;
}

private boolean isAuthDataMode() { // Authenticated Encryption with Associated Data (AEAD)
return "GCM".equalsIgnoreCase(cryptoMode) || "CCM".equalsIgnoreCase(cryptoMode);
}

private static final int MAX_AUTH_TAG_LENGTH = 16;

private int getAuthTagLength() {
return Math.min(MAX_AUTH_TAG_LENGTH, this.key.length); // in bytes
return Math.min(auth_tag_len, this.key.length); // in bytes
}

private transient ByteList auth_data;
Expand Down Expand Up @@ -1346,22 +1362,10 @@ public IRubyObject random_iv(final ThreadContext context) {
this.set_iv(context, str); return str;
}

//String getAlgorithm() {
// return this.cipher.getAlgorithm();
//}

final String getName() {
return this.name;
}

//String getCryptoBase() {
// return this.cryptoBase;
//}

//String getCryptoMode() {
// return this.cryptoMode;
//}

final int getKeyLength() {
return keyLength;
}
Expand Down
102 changes: 88 additions & 14 deletions src/test/ruby/test_cipher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -363,41 +363,39 @@ def test_aes_128_gcm
#assert_equal "", cipher.final
end

def test_aes_gcm
def test_aes_gcm_custom
['aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm'].each do |algo|
pt = "You should all use Authenticated Encryption!"
cipher, key, iv = new_encryptor(algo)
cipher, key, iv = new_random_encryptor(algo)

cipher.auth_data = "aad"
ct = cipher.update(pt) + cipher.final
tag = cipher.auth_tag
assert_equal(16, tag.size)

decipher = new_decryptor(algo, key, iv)
decipher = new_decryptor(algo, key: key, iv: iv)
decipher.auth_tag = tag
decipher.auth_data = "aad"

assert_equal(pt, decipher.update(ct) + decipher.final)
end
end

def new_encryptor(algo)
def test_authenticated
cipher = OpenSSL::Cipher.new('aes-128-gcm')
assert_predicate(cipher, :authenticated?)
cipher = OpenSSL::Cipher.new('aes-128-cbc')
assert_not_predicate(cipher, :authenticated?)
end

def new_random_encryptor(algo)
cipher = OpenSSL::Cipher.new(algo)
cipher.encrypt
key = cipher.random_key
iv = cipher.random_iv
[cipher, key, iv]
end
private :new_encryptor

def new_decryptor(algo, key, iv)
OpenSSL::Cipher.new(algo).tap do |cipher|
cipher.decrypt
cipher.key = key
cipher.iv = iv
end
end
private :new_decryptor
private :new_random_encryptor

def test_aes_128_gcm_with_auth_tag
cipher = OpenSSL::Cipher.new('aes-128-gcm')
Expand Down Expand Up @@ -498,4 +496,80 @@ def test_encrypt_aes_256_cbc_invalid_buffer
assert_raise(TypeError) { cipher.update('bar' * 10, buffer) }
end

def test_aes_gcm
# GCM spec Appendix B Test Case 4
key = ["feffe9928665731c6d6a8f9467308308"].pack("H*")
iv = ["cafebabefacedbaddecaf888"].pack("H*")
aad = ["feedfacedeadbeeffeedfacedeadbeef" \
"abaddad2"].pack("H*")
pt = ["d9313225f88406e5a55909c5aff5269a" \
"86a7a9531534f7da2e4c303d8a318a72" \
"1c3c0c95956809532fcf0e2449a6b525" \
"b16aedf5aa0de657ba637b39"].pack("H*")
ct = ["42831ec2217774244b7221b784d0d49c" \
"e3aa212f2c02a4e035c17e2329aca12e" \
"21d514b25466931c7d8f6a5aac84aa05" \
"1ba30b396a0aac973d58e091"].pack("H*")
tag = ["5bc94fbc3221a5db94fae95ae7121a47"].pack("H*")

cipher = new_encryptor("aes-128-gcm", key: key, iv: iv, auth_data: aad)
# TODO JOpenSSL should raise
# assert_raise(OpenSSL::Cipher::CipherError, 'unable to set authentication tag length: failed to get parameter') do
# cipher.auth_tag_len = 16
# end
assert_equal ct, cipher.update(pt) << cipher.final
assert_equal tag, cipher.auth_tag
cipher = new_decryptor("aes-128-gcm", key: key, iv: iv, auth_tag: tag, auth_data: aad)
# TODO JOpenSSL should raise
# assert_raise(OpenSSL::Cipher::CipherError, 'unable to set authentication tag length: failed to get parameter') do
# cipher.auth_tag_len = 16
# end
assert_equal pt, cipher.update(ct) << cipher.final

# truncated tag is accepted
cipher = new_encryptor("aes-128-gcm", key: key, iv: iv, auth_data: aad)
assert_equal ct, cipher.update(pt) << cipher.final
assert_equal tag[0, 8], cipher.auth_tag(8)
assert_equal tag, cipher.auth_tag

# NOTE: MRI seems to just ignore the invalid tag?!
# cipher = new_decryptor("aes-128-gcm", key: key, iv: iv, auth_tag: tag[0, 8], auth_data: aad)
# assert_equal pt, cipher.update(ct) << cipher.final

# wrong tag is rejected
tag2 = tag.dup
tag2.setbyte(-1, (tag2.getbyte(-1) + 1) & 0xff)
cipher = new_decryptor("aes-128-gcm", key: key, iv: iv, auth_tag: tag2, auth_data: aad)
cipher.update(ct)
assert_raise(OpenSSL::Cipher::CipherError) { cipher.final }

# wrong aad is rejected
aad2 = aad[0..-2] << aad[-1].succ
cipher = new_decryptor("aes-128-gcm", key: key, iv: iv, auth_tag: tag, auth_data: aad2)
cipher.update(ct)
assert_raise(OpenSSL::Cipher::CipherError) { cipher.final }

# wrong ciphertext is rejected
ct2 = ct[0..-2] << ct[-1].succ
cipher = new_decryptor("aes-128-gcm", key: key, iv: iv, auth_tag: tag, auth_data: aad)
cipher.update(ct2)
assert_raise(OpenSSL::Cipher::CipherError) { cipher.final }
end

private

def new_encryptor(algo, **kwargs)
OpenSSL::Cipher.new(algo).tap do |cipher|
cipher.encrypt
kwargs.each {|k, v| cipher.send(:"#{k}=", v) }
end
end

def new_decryptor(algo, **kwargs)
OpenSSL::Cipher.new(algo).tap do |cipher|
cipher.decrypt
kwargs.each {|k, v| cipher.send(:"#{k}=", v) }
end
end

end