Skip to content

Commit 9173891

Browse files
committed
need to implement Cipher#auth_tag for GCM (only encrypting for now)
1 parent 7e53018 commit 9173891

File tree

2 files changed

+120
-6
lines changed

2 files changed

+120
-6
lines changed

src/main/java/org/jruby/ext/openssl/Cipher.java

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252

5353
import org.jruby.Ruby;
5454
import org.jruby.RubyArray;
55+
import org.jruby.RubyBoolean;
5556
import org.jruby.RubyClass;
5657
import org.jruby.RubyInteger;
5758
import org.jruby.RubyModule;
@@ -616,7 +617,7 @@ public int getIvLength() {
616617
if ( "AES".equals(base) ) {
617618
ivLength = 16; // OpenSSL defaults to 12
618619
// NOTE: we can NOT handle 12 for non GCM mode
619-
if ( "GCM".equals(mode) ) ivLength = 12;
620+
if ( "GCM".equals(mode) || "CCM".equals(mode) ) ivLength = 12;
620621
}
621622
//else if ( "DES".equals(base) ) {
622623
// ivLength = 8;
@@ -1047,7 +1048,7 @@ else if ( "RC4".equalsIgnoreCase(cryptoBase) ) {
10471048
else {
10481049
final AlgorithmParameterSpec ivSpec;
10491050
if ( "GCM".equalsIgnoreCase(cryptoMode) ) { // e.g. 'aes-128-gcm'
1050-
ivSpec = new GCMParameterSpec(this.ivLength * 8, this.realIV);
1051+
ivSpec = new GCMParameterSpec(getAuthTagLength() * 8, this.realIV);
10511052
}
10521053
else {
10531054
ivSpec = new IvParameterSpec(this.realIV);
@@ -1083,6 +1084,9 @@ private String getCipherAlgorithm() {
10831084
@JRubyMethod
10841085
public IRubyObject update(final ThreadContext context, final IRubyObject arg) {
10851086
final Ruby runtime = context.runtime;
1087+
1088+
if ( auth_tag != null ) throw newAuthTagPresentError(runtime);
1089+
10861090
if ( isDebug(runtime) ) dumpVars( runtime.getOut(), "update()" );
10871091

10881092
checkCipherNotNull(runtime);
@@ -1133,22 +1137,41 @@ public IRubyObject update_deprecated(final ThreadContext context, final IRubyObj
11331137
@JRubyMethod(name = "final")
11341138
public IRubyObject do_final(final ThreadContext context) {
11351139
final Ruby runtime = context.runtime;
1140+
1141+
if ( auth_tag != null ) throw newAuthTagPresentError(runtime);
1142+
11361143
checkCipherNotNull(runtime);
11371144
if ( ! cipherInited ) doInitCipher(runtime);
11381145
// trying to allow update after final like cruby-openssl. Bad idea.
11391146
if ( "RC4".equalsIgnoreCase(cryptoBase) ) return runtime.newString("");
11401147

1141-
final ByteList str;
1148+
final ByteList str; boolean shared = false;
11421149
try {
11431150
final byte[] out = cipher.doFinal();
11441151
if ( out != null ) {
1145-
str = new ByteList(out, false);
1152+
if ( isAuthDataMode() ) { // TODO only implemented encryption !
1153+
1154+
// if GCM/CCM is being used, the authentication tag is appended
1155+
// in the case of encryption, or verified in the case of decryption.
1156+
// The result is stored in a new buffer.
1157+
final int len = getAuthTagLength(); int strLen;
1158+
if ( ( strLen = out.length - len ) > 0 ) {
1159+
str = new ByteList(out, 0, strLen, false); shared = true;
1160+
}
1161+
else {
1162+
str = new ByteList(ByteList.NULL_ARRAY); strLen = 0;
1163+
}
1164+
auth_tag = new ByteList(out, strLen, out.length - strLen);
1165+
}
11461166
// TODO: Modifying this line appears to fix the issue, but I do
11471167
// not have a good reason for why. Best I can tell, lastIv needs
11481168
// to be set regardless of encryptMode, so we'll go with this
11491169
// for now. JRUBY-3335.
11501170
//if ( realIV != null && encryptMode ) ...
1151-
if ( realIV != null ) setLastIVIfNeeded(out);
1171+
else {
1172+
str = new ByteList(out, false);
1173+
if ( realIV != null ) setLastIVIfNeeded(out);
1174+
}
11521175
}
11531176
else {
11541177
str = new ByteList(ByteList.NULL_ARRAY);
@@ -1174,7 +1197,18 @@ public IRubyObject do_final(final ThreadContext context) {
11741197
debugStackTrace(runtime, e);
11751198
throw newCipherError(runtime, e);
11761199
}
1177-
return RubyString.newString(runtime, str);
1200+
return shared ? RubyString.newStringShared(runtime, str) : RubyString.newString(runtime, str);
1201+
}
1202+
1203+
private RaiseException newAuthTagPresentError(final Ruby runtime) {
1204+
final String error;
1205+
//if ( encryptMode ) {
1206+
error = "authentication tag already generated by cipher";
1207+
//}
1208+
//else {
1209+
//error = "authentication tag already consumed by cipher";
1210+
//}
1211+
return newCipherError(runtime, error);
11781212
}
11791213

11801214
private void setLastIVIfNeeded(final byte[] tmpIV) {
@@ -1195,6 +1229,34 @@ public IRubyObject set_padding(IRubyObject padding) {
11951229
return padding;
11961230
}
11971231

1232+
private transient ByteList auth_tag;
1233+
1234+
@JRubyMethod(name = "auth_tag")
1235+
public IRubyObject auth_tag(final ThreadContext context) {
1236+
if ( auth_tag != null ) {
1237+
return RubyString.newString(context.runtime, auth_tag);
1238+
}
1239+
if ( ! isAuthDataMode() ) {
1240+
throw newCipherError(context.runtime, "authentication tag not supported by this cipher");
1241+
}
1242+
return context.nil;
1243+
}
1244+
1245+
private boolean isAuthDataMode() { // Authenticated Encryption with Associated Data (AEAD)
1246+
return "GCM".equalsIgnoreCase(cryptoMode) || "CCM".equalsIgnoreCase(cryptoMode);
1247+
}
1248+
1249+
private static final int MAX_AUTH_TAG_LENGTH = 16;
1250+
1251+
private int getAuthTagLength() {
1252+
return Math.min(MAX_AUTH_TAG_LENGTH, this.key.length); // in bytes
1253+
}
1254+
1255+
@JRubyMethod(name = "authenticated?")
1256+
public RubyBoolean authenticated_p(final ThreadContext context) {
1257+
return context.runtime.newBoolean( isAuthDataMode() );
1258+
}
1259+
11981260
@JRubyMethod
11991261
public IRubyObject random_key(final ThreadContext context) {
12001262
// str = OpenSSL::Random.random_bytes(self.key_len)

src/test/ruby/test_cipher.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def test_instantiate_supported_ciphers
6868
#puts OpenSSL::Cipher.ciphers.size
6969

7070
OpenSSL::Cipher.ciphers.each do |cipher_name|
71+
next if cipher_name.end_with?('wrap') # e.g. 'id-aes256-wrap'
7172
OpenSSL::Cipher.new cipher_name
7273
end
7374
end
@@ -347,6 +348,57 @@ def test_aes_128_gcm
347348
actual = cipher.update(bytes)
348349
assert_equal expected, actual
349350
#assert_equal "", cipher.final
351+
352+
cipher = OpenSSL::Cipher.new('aes-256-gcm')
353+
assert_equal cipher, cipher.encrypt
354+
assert_equal 32, cipher.key_len
355+
assert_equal 12, cipher.iv_len
356+
cipher.key = '01245678' * 4
357+
cipher.iv = '0123456' * 2
358+
359+
bytes = '0101' * 8
360+
expected = "\xA8I0\xF8\xCD?Z\xFD\x8E\"T\xF5\xF2\xC5\xC8\x05\xD4b\x85\xA3}'\xC99]\xC1\x16\x8B\x13\x9E-)" # from MRI
361+
actual = cipher.update(bytes)
362+
assert_equal expected, actual
363+
#assert_equal "", cipher.final
364+
end
365+
366+
def test_aes_128_gcm_with_auth_tag
367+
cipher = OpenSSL::Cipher.new('aes-128-gcm')
368+
cipher.encrypt
369+
#assert_equal 16, cipher.key_len
370+
#assert_equal 12, cipher.iv_len
371+
cipher.key = '01' * 8
372+
cipher.iv = '1001' * 3
373+
374+
plaintext = "Hello World"
375+
376+
padding = cipher.update("\0\0")
377+
text = cipher.update(plaintext)
378+
379+
final = cipher.final; a_tag = cipher.auth_tag
380+
381+
assert_equal "\xB5\xFD", padding unless defined? JRUBY_VERSION
382+
assert_equal "\xCCxqd\xDE\x92\x95\xAD0\xB4=", text unless defined? JRUBY_VERSION
383+
assert_equal "", final unless defined? JRUBY_VERSION
384+
385+
assert_equal "\xB5\xFD\xCCxqd\xDE\x92\x95\xAD0\xB4=", padding + text + final
386+
387+
assert_equal "\ay\xBA\x89\xC9\x91\xF8N\xB7\xD6\x17+\x0F\\\xF8N", a_tag
388+
389+
assert_equal a_tag, cipher.auth_tag
390+
assert_raise(OpenSSL::Cipher::CipherError) { cipher.update("\0\0") }
391+
assert_equal a_tag, cipher.auth_tag
392+
assert_raise(OpenSSL::Cipher::CipherError) { cipher.final }
393+
end
394+
395+
def test_encrypt_auth_data_non_gcm
396+
cipher = OpenSSL::Cipher.new 'aes-128-cfb'
397+
cipher.encrypt
398+
#length = 16
399+
#cipher.iv = '0' * length
400+
#cipher.key = '1' * length
401+
assert_raise(OpenSSL::Cipher::CipherError) { cipher.auth_tag }
350402
end
351403

352404
def test_encrypt_aes_cfb_16_incompatibility

0 commit comments

Comments
 (0)