Skip to content

Commit

Permalink
[fix] add Cipher#auth_data(arg) override (Rails 7.x compat)
Browse files Browse the repository at this point in the history
  • Loading branch information
kares committed Feb 12, 2024
1 parent 1913525 commit 5981217
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 31 deletions.
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

0 comments on commit 5981217

Please sign in to comment.