52
52
53
53
import org .jruby .Ruby ;
54
54
import org .jruby .RubyArray ;
55
+ import org .jruby .RubyBoolean ;
55
56
import org .jruby .RubyClass ;
56
57
import org .jruby .RubyInteger ;
57
58
import org .jruby .RubyModule ;
@@ -616,7 +617,7 @@ public int getIvLength() {
616
617
if ( "AES" .equals (base ) ) {
617
618
ivLength = 16 ; // OpenSSL defaults to 12
618
619
// 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 ;
620
621
}
621
622
//else if ( "DES".equals(base) ) {
622
623
// ivLength = 8;
@@ -1047,7 +1048,7 @@ else if ( "RC4".equalsIgnoreCase(cryptoBase) ) {
1047
1048
else {
1048
1049
final AlgorithmParameterSpec ivSpec ;
1049
1050
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 );
1051
1052
}
1052
1053
else {
1053
1054
ivSpec = new IvParameterSpec (this .realIV );
@@ -1083,6 +1084,9 @@ private String getCipherAlgorithm() {
1083
1084
@ JRubyMethod
1084
1085
public IRubyObject update (final ThreadContext context , final IRubyObject arg ) {
1085
1086
final Ruby runtime = context .runtime ;
1087
+
1088
+ if ( auth_tag != null ) throw newAuthTagPresentError (runtime );
1089
+
1086
1090
if ( isDebug (runtime ) ) dumpVars ( runtime .getOut (), "update()" );
1087
1091
1088
1092
checkCipherNotNull (runtime );
@@ -1133,22 +1137,41 @@ public IRubyObject update_deprecated(final ThreadContext context, final IRubyObj
1133
1137
@ JRubyMethod (name = "final" )
1134
1138
public IRubyObject do_final (final ThreadContext context ) {
1135
1139
final Ruby runtime = context .runtime ;
1140
+
1141
+ if ( auth_tag != null ) throw newAuthTagPresentError (runtime );
1142
+
1136
1143
checkCipherNotNull (runtime );
1137
1144
if ( ! cipherInited ) doInitCipher (runtime );
1138
1145
// trying to allow update after final like cruby-openssl. Bad idea.
1139
1146
if ( "RC4" .equalsIgnoreCase (cryptoBase ) ) return runtime .newString ("" );
1140
1147
1141
- final ByteList str ;
1148
+ final ByteList str ; boolean shared = false ;
1142
1149
try {
1143
1150
final byte [] out = cipher .doFinal ();
1144
1151
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
+ }
1146
1166
// TODO: Modifying this line appears to fix the issue, but I do
1147
1167
// not have a good reason for why. Best I can tell, lastIv needs
1148
1168
// to be set regardless of encryptMode, so we'll go with this
1149
1169
// for now. JRUBY-3335.
1150
1170
//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
+ }
1152
1175
}
1153
1176
else {
1154
1177
str = new ByteList (ByteList .NULL_ARRAY );
@@ -1174,7 +1197,18 @@ public IRubyObject do_final(final ThreadContext context) {
1174
1197
debugStackTrace (runtime , e );
1175
1198
throw newCipherError (runtime , e );
1176
1199
}
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 );
1178
1212
}
1179
1213
1180
1214
private void setLastIVIfNeeded (final byte [] tmpIV ) {
@@ -1195,6 +1229,34 @@ public IRubyObject set_padding(IRubyObject padding) {
1195
1229
return padding ;
1196
1230
}
1197
1231
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
+
1198
1260
@ JRubyMethod
1199
1261
public IRubyObject random_key (final ThreadContext context ) {
1200
1262
// str = OpenSSL::Random.random_bytes(self.key_len)
0 commit comments