@@ -1038,6 +1038,12 @@ private void doInitCipher(final Ruby runtime) {
10381038 if ( key == null ) { //key = emptyKey(keyLength);
10391039 throw newCipherError (runtime , "key not specified" );
10401040 }
1041+ // If a previous encrypt pass swapped in a NoPadding cipher, restore
1042+ // the original PKCS5Padding cipher before re-initializing.
1043+ if ( updateBuffer != null ) {
1044+ this .cipher = getCipherInstance ();
1045+ updateBuffer = null ;
1046+ }
10411047 try {
10421048 // ECB mode is the only mode that does not require an IV
10431049 if ( "ECB" .equalsIgnoreCase (cryptoMode ) ) {
@@ -1084,6 +1090,35 @@ else if ( "RC4".equalsIgnoreCase(cryptoBase) ) {
10841090 }
10851091 cipherInited = true ;
10861092 processedDataBytes = 0 ;
1093+
1094+ if ( needsManualBlockBuffering () ) {
1095+ // Switch JCE cipher to NoPadding so it flushes complete blocks
1096+ // immediately. We'll add PKCS7 padding ourselves in do_final.
1097+ final int blockSize = cipher .getBlockSize ();
1098+ if ( blockSize > 0 ) {
1099+ final String noPadName = realName .replace ("PKCS5Padding" , "NoPadding" );
1100+ try {
1101+ javax .crypto .Cipher noPad = getCipherInstance (noPadName , false );
1102+ if ( "ECB" .equalsIgnoreCase (cryptoMode ) ) {
1103+ noPad .init (ENCRYPT_MODE , new SimpleSecretKey (getCipherAlgorithm (), this .key ));
1104+ } else {
1105+ noPad .init (ENCRYPT_MODE ,
1106+ new SimpleSecretKey (getCipherAlgorithm (), this .key ),
1107+ new IvParameterSpec (this .realIV ));
1108+ }
1109+ this .cipher = noPad ;
1110+ updateBuffer = new byte [blockSize ];
1111+ updateBufferPos = 0 ;
1112+ }
1113+ catch (Exception e ) {
1114+ // fall back to default behavior if NoPadding variant unavailable
1115+ debugStackTrace (runtime , e );
1116+ updateBuffer = null ;
1117+ }
1118+ }
1119+ } else {
1120+ updateBuffer = null ;
1121+ }
10871122 }
10881123
10891124 private String getCipherAlgorithm () {
@@ -1094,6 +1129,15 @@ private String getCipherAlgorithm() {
10941129 private int processedDataBytes = 0 ;
10951130 private byte [] lastIV ;
10961131
1132+ // C OpenSSL's EVP_EncryptUpdate flushes complete blocks immediately,
1133+ // while Java's Cipher.update with PKCS5Padding holds back the last
1134+ // complete block for potential padding merge in doFinal. We buffer
1135+ // partial blocks ourselves and feed only complete blocks to Java
1136+ // (configured with NoPadding), adding PKCS7 padding manually in final.
1137+ // This is only active for padded block cipher encryption (CBC, ECB).
1138+ private byte [] updateBuffer ;
1139+ private int updateBufferPos ;
1140+
10971141 @ JRubyMethod
10981142 public IRubyObject update (final ThreadContext context , final IRubyObject arg ) {
10991143 return update (context , arg , null );
@@ -1122,19 +1166,26 @@ public IRubyObject update(final ThreadContext context, final IRubyObject arg, IR
11221166
11231167 final byte [] in = data .getUnsafeBytes ();
11241168 final int offset = data .begin ();
1125- final byte [] out = cipher .update (in , offset , length );
1126- if ( out != null ) {
1127- str = new ByteList (out , false );
1128- if ( realIV != null ) {
1129- if ( encryptMode ) setLastIVIfNeeded ( out );
1130- else setLastIVIfNeeded ( in , offset , length );
1131- }
11321169
1133- processedDataBytes += length ;
1170+ if ( updateBuffer != null ) {
1171+ // Manual block buffering for padded block cipher encryption.
1172+ // Feed only complete blocks to NoPadding cipher, buffer the rest.
1173+ str = updateWithManualBuffering (in , offset , length );
11341174 }
11351175 else {
1136- str = new ByteList (ByteList .NULL_ARRAY );
1176+ final byte [] out = cipher .update (in , offset , length );
1177+ if ( out != null ) {
1178+ str = new ByteList (out , false );
1179+ if ( realIV != null ) {
1180+ if ( encryptMode ) setLastIVIfNeeded ( out );
1181+ else setLastIVIfNeeded ( in , offset , length );
1182+ }
1183+ }
1184+ else {
1185+ str = new ByteList (ByteList .NULL_ARRAY );
1186+ }
11371187 }
1188+ processedDataBytes += length ;
11381189 }
11391190 catch (Exception e ) {
11401191 debugStackTrace ( runtime , e );
@@ -1148,6 +1199,48 @@ public IRubyObject update(final ThreadContext context, final IRubyObject arg, IR
11481199 return buffer ;
11491200 }
11501201
1202+ private ByteList updateWithManualBuffering (final byte [] in , int offset , int length ) {
1203+ final int blockSize = updateBuffer .length ;
1204+ // Merge input with any previously buffered partial block
1205+ final int total = updateBufferPos + length ;
1206+ final int fullBlocks = total / blockSize ;
1207+ final int remainder = total % blockSize ;
1208+
1209+ if ( fullBlocks == 0 ) {
1210+ // Not enough for a complete block — just buffer
1211+ System .arraycopy (in , offset , updateBuffer , updateBufferPos , length );
1212+ updateBufferPos = total ;
1213+ return new ByteList (ByteList .NULL_ARRAY );
1214+ }
1215+
1216+ // Build a byte array of complete blocks to feed to the cipher
1217+ final byte [] toEncrypt = new byte [fullBlocks * blockSize ];
1218+ int pos = 0 ;
1219+ // First copy any previously buffered bytes
1220+ if ( updateBufferPos > 0 ) {
1221+ System .arraycopy (updateBuffer , 0 , toEncrypt , 0 , updateBufferPos );
1222+ pos = updateBufferPos ;
1223+ }
1224+ // Then copy from input up to the full-block boundary
1225+ final int fromInput = fullBlocks * blockSize - pos ;
1226+ System .arraycopy (in , offset , toEncrypt , pos , fromInput );
1227+ offset += fromInput ;
1228+
1229+ // Buffer the remainder for next update/final
1230+ if ( remainder > 0 ) {
1231+ System .arraycopy (in , offset , updateBuffer , 0 , remainder );
1232+ }
1233+ updateBufferPos = remainder ;
1234+
1235+ // Encrypt complete blocks (NoPadding flushes immediately)
1236+ final byte [] out = cipher .update (toEncrypt );
1237+ if ( out != null ) {
1238+ if ( realIV != null ) setLastIVIfNeeded (out );
1239+ return new ByteList (out , false );
1240+ }
1241+ return new ByteList (ByteList .NULL_ARRAY );
1242+ }
1243+
11511244 @ JRubyMethod (name = "<<" )
11521245 public IRubyObject update_deprecated (final ThreadContext context , final IRubyObject data ) {
11531246 context .runtime .getWarnings ().warn (ID .DEPRECATED_METHOD , getMetaClass ().getRealClass ().getName () + "#<< is deprecated; use #update instead" );
@@ -1170,6 +1263,9 @@ public IRubyObject do_final(final ThreadContext context) {
11701263 if ( isAuthDataMode () ) {
11711264 str = do_final_with_auth (runtime );
11721265 }
1266+ else if ( updateBuffer != null ) {
1267+ str = do_final_with_manual_padding ();
1268+ }
11731269 else {
11741270 final byte [] out = cipher .doFinal ();
11751271 if ( out != null ) {
@@ -1209,6 +1305,27 @@ public IRubyObject do_final(final ThreadContext context) {
12091305 return RubyString .newString (runtime , str );
12101306 }
12111307
1308+ private ByteList do_final_with_manual_padding () throws GeneralSecurityException {
1309+ // Add PKCS7 padding to buffered partial block and encrypt.
1310+ // If buffer is empty (0 bytes remaining), a full padding block is added.
1311+ final int blockSize = updateBuffer .length ;
1312+ final int padLen = blockSize - updateBufferPos ;
1313+ final byte [] lastBlock = new byte [blockSize ];
1314+ if ( updateBufferPos > 0 ) {
1315+ System .arraycopy (updateBuffer , 0 , lastBlock , 0 , updateBufferPos );
1316+ }
1317+ // PKCS7: fill remaining bytes with the pad length value
1318+ java .util .Arrays .fill (lastBlock , updateBufferPos , blockSize , (byte ) padLen );
1319+
1320+ final byte [] out = cipher .doFinal (lastBlock );
1321+ updateBufferPos = 0 ;
1322+ if ( out != null ) {
1323+ if ( realIV != null ) setLastIVIfNeeded (out );
1324+ return new ByteList (out , false );
1325+ }
1326+ return new ByteList (ByteList .NULL_ARRAY );
1327+ }
1328+
12121329 private ByteList do_final_with_auth (final Ruby runtime ) throws GeneralSecurityException {
12131330 updateAuthData (runtime ); // if any
12141331
@@ -1387,6 +1504,15 @@ private boolean isStreamCipher() {
13871504 return cipher .getBlockSize () == 0 ;
13881505 }
13891506
1507+ // True when we need manual block buffering to match C OpenSSL's
1508+ // EVP_EncryptUpdate flush behavior (see updateBuffer field comment).
1509+ private boolean needsManualBlockBuffering () {
1510+ return encryptMode
1511+ && "PKCS5Padding" .equals (paddingType )
1512+ && !isStreamCipher ()
1513+ && !isAuthDataMode ();
1514+ }
1515+
13901516 private static RaiseException newCipherError (Ruby runtime , Exception e ) {
13911517 return Utils .newError (runtime , _Cipher (runtime ).getClass ("CipherError" ), e );
13921518 }
0 commit comments