diff --git a/src/main/java/org/jruby/ext/openssl/Cipher.java b/src/main/java/org/jruby/ext/openssl/Cipher.java index 220d3606..2c2af929 100644 --- a/src/main/java/org/jruby/ext/openssl/Cipher.java +++ b/src/main/java/org/jruby/ext/openssl/Cipher.java @@ -826,6 +826,8 @@ public IRubyObject initialize_copy(final IRubyObject obj) { } this.orgIV = this.realIV; padding = other.padding; + ctrNextIV = null; + ctrBlockOffset = 0; cipher = getCipherInstance(); @@ -908,6 +910,10 @@ public IRubyObject set_iv(final ThreadContext context, final IRubyObject iv) { System.arraycopy(ivBytes.unsafeBytes(), ivBytes.getBegin(), i, 0, ivLength); this.realIV = i; this.orgIV = this.realIV; + if (isCTRMode()) { + ctrNextIV = Arrays.copyOf(i, i.length); + ctrBlockOffset = 0; + } if ( ! isStreamCipher() ) cipherInited = false; if ( encryptMode ) auth_tag = null; @@ -933,6 +939,8 @@ private void init(final ThreadContext context, final IRubyObject[] args, final b if ( encryptMode ) auth_tag = null; // stale from a previous encryption encryptMode = encrypt; cipherInited = false; + ctrNextIV = null; + ctrBlockOffset = 0; if ( args.length > 0 ) { /* @@ -991,7 +999,16 @@ public IRubyObject reset(final ThreadContext context) { try { checkCipherNotNull(runtime); if ( ! isStreamCipher() ) { - this.realIV = orgIV; + if ( isCTRMode() && key != null ) { + if (ctrNextIV == null) { + ctrNextIV = realIV == null ? new byte[ivLength] : Arrays.copyOf(realIV, ivLength); + } + ctrBlockOffset = 0; + this.realIV = Arrays.copyOf(ctrNextIV, ivLength); + } + else { + this.realIV = orgIV; + } // CRuby's reset calls EVP_CipherInit_ex with NULL key/IV which // is a no-op if key hasn't been set yet. Match that behavior. if ( key != null ) doInitCipher(runtime); @@ -1022,6 +1039,8 @@ private void updateCipher(final String name, final String padding) { cryptoMode = alg.mode; realName = alg.getRealName(); paddingType = alg.getPadding(); + ctrNextIV = null; + ctrBlockOffset = 0; keyLength = alg.getKeyLength(); ivLength = alg.getIvLength(); @@ -1084,6 +1103,8 @@ public IRubyObject pkcs5_keyivgen(final ThreadContext context, final IRubyObject this.key = result.key; this.realIV = result.iv; this.orgIV = this.realIV; + this.ctrNextIV = null; + this.ctrBlockOffset = 0; doInitCipher(runtime); @@ -1157,6 +1178,9 @@ else if ( "RC4".equalsIgnoreCase(cryptoBase) ) { cipherInited = true; processedDataBytes = 0; outputDataBytes = 0; + if ( isCTRMode() && ctrNextIV == null ) { + ctrNextIV = Arrays.copyOf(realIV, ivLength); + } if ( switchToManualBlockUpdateBuffer() ) { // switch JCE cipher to NoPadding so it flushes complete blocks immediately; add PKCS7 padding in do_final @@ -1187,6 +1211,8 @@ private String getCipherAlgorithm() { private int processedDataBytes = 0; private int outputDataBytes = 0; + private byte[] ctrNextIV; + private int ctrBlockOffset = 0; private byte[] lastIV; @JRubyMethod @@ -1235,6 +1261,7 @@ public IRubyObject update(final ThreadContext context, final IRubyObject arg, IR str = new ByteList(ByteList.NULL_ARRAY); } } + if ( isCTRMode() ) advanceCtrPosition(length); processedDataBytes += length; } catch (Exception e) { @@ -1252,6 +1279,17 @@ public IRubyObject update(final ThreadContext context, final IRubyObject arg, IR return buffer; } + private void advanceCtrPosition(final int length) { + final long bytes = (long) ctrBlockOffset + length; + long blocks = (bytes + ivLength - 1) / ivLength - (ctrBlockOffset == 0 ? 0 : 1); + for ( int i = ctrNextIV.length - 1; i >= 0 && blocks != 0; i-- ) { + blocks += ctrNextIV[i] & 0xFFL; + ctrNextIV[i] = (byte) blocks; + blocks >>>= 8; + } + ctrBlockOffset = (int) (bytes % ivLength); + } + private ByteList updateWithBuffer(final byte[] in, int offset, int length) throws GeneralSecurityException { final int blockSize = updateBuffer.length; final int total = updateBufferPos + length; @@ -1319,8 +1357,7 @@ public IRubyObject do_final(final ThreadContext context) { if ( "RC4".equalsIgnoreCase(cryptoBase) ) return RubyString.newEmptyString(runtime); // complete CTR and OFB blocks have nothing to flush (doFinal would reset the state) - if ( ( "CTR".equalsIgnoreCase(cryptoMode) || "OFB".equalsIgnoreCase(cryptoMode) ) && - "NoPadding".equals(paddingType) && + if ( ( isCTRMode() || isOFBMode() ) && "NoPadding".equals(paddingType) && processedDataBytes == outputDataBytes && processedDataBytes % ivLength == 0 ) { return RubyString.newEmptyString(runtime); } @@ -1607,6 +1644,14 @@ private boolean isStreamCipher() { return cipher.getBlockSize() == 0; } + private boolean isCTRMode() { + return "CTR".equalsIgnoreCase(cryptoMode); + } + + private boolean isOFBMode() { + return "OFB".equalsIgnoreCase(cryptoMode); + } + private static RaiseException newCipherError(Ruby runtime, Exception e) { return newError(runtime, _Cipher(runtime).getClass("CipherError"), e); } diff --git a/test/test_cipher.rb b/test/test_cipher.rb index b569b0e5..de44ef8e 100644 --- a/test/test_cipher.rb +++ b/test/test_cipher.rb @@ -434,6 +434,60 @@ def test_aes_ctr_continues_after_final_on_block_boundary end end + def test_ctr_reset_continues_at_next_block + plain = 'x' * 96 + + algos = %w[aes-128-ctr aes-192-ctr aes-256-ctr] + algos << 'camellia-128-ctr' unless fips? + algos << 'bf-ctr' if defined?(JRUBY_VERSION) && !fips? + algos.each do |algo| + base = OpenSSL::Cipher.new(algo) + key = 'k' * base.key_len + iv = ['000102030405060708090a0b0c0dfeff'].pack('H*').byteslice(-base.iv_len, base.iv_len) + whole = OpenSSL::Cipher.new(algo).encrypt; whole.key = key; whole.iv = iv + encrypted = whole.update(plain) + whole.final + + [0, 5, base.iv_len, base.iv_len + 1, base.iv_len * 2].each do |length| + next_offset = ((length + base.iv_len - 1) / base.iv_len) * base.iv_len + [false, true].each do |finish| + enc = OpenSSL::Cipher.new(algo).encrypt; enc.key = key; enc.iv = iv + enc.update(plain.byteslice(0, length)) if length > 0 + enc.final if finish + enc.reset + enc.reset + assert_equal encrypted.byteslice(next_offset, base.iv_len), + enc.update(plain.byteslice(next_offset, base.iv_len)) + enc.final, + "#{algo} encrypt reset after #{length} bytes (final=#{finish})" + + dec = OpenSSL::Cipher.new(algo).decrypt; dec.key = key; dec.iv = iv + dec.update(encrypted.byteslice(0, length)) if length > 0 + dec.final if finish + dec.reset + assert_equal plain.byteslice(next_offset, base.iv_len), + dec.update(encrypted.byteslice(next_offset, base.iv_len)) + dec.final, + "#{algo} decrypt reset after #{length} bytes (final=#{finish})" + end + end + + split = OpenSSL::Cipher.new(algo).encrypt; split.key = key; split.iv = iv + split.update(plain.byteslice(0, 5)) + split.update(plain.byteslice(5, 12)) + split.reset + next_offset = ((17 + base.iv_len - 1) / base.iv_len) * base.iv_len + assert_equal encrypted.byteslice(next_offset, base.iv_len), + split.update(plain.byteslice(next_offset, base.iv_len)) + split.final, + "#{algo} reset after split updates" + + rewound = OpenSSL::Cipher.new(algo).encrypt; rewound.key = key; rewound.iv = iv + rewound.update(plain.byteslice(0, base.iv_len)) + rewound.reset + rewound.iv = iv + assert_equal encrypted.byteslice(0, base.iv_len), + rewound.update(plain.byteslice(0, base.iv_len)) + rewound.final, + "#{algo} explicit IV after reset" + end + end + # NIST SP 800-38A F.5.5/F.5.6 AES-256-CTR known-answer vector def test_aes256_ctr_nist_kat key = ['603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4'].pack('H*')