5252
5353import org .jruby .Ruby ;
5454import org .jruby .RubyArray ;
55+ import org .jruby .RubyBoolean ;
5556import org .jruby .RubyClass ;
5657import org .jruby .RubyInteger ;
5758import 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)
0 commit comments