Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 48 additions & 3 deletions src/main/java/org/jruby/ext/openssl/Cipher.java
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,8 @@ public IRubyObject initialize_copy(final IRubyObject obj) {
}
this.orgIV = this.realIV;
padding = other.padding;
ctrNextIV = null;
ctrBlockOffset = 0;

cipher = getCipherInstance();

Expand Down Expand Up @@ -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;
Expand All @@ -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 ) {
/*
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down
54 changes: 54 additions & 0 deletions test/test_cipher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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*')
Expand Down
Loading